1284 lines
36 KiB
TypeScript
1284 lines
36 KiB
TypeScript
import express from "express";
|
|
import cors from "cors";
|
|
import { Client as SSHClient } from "ssh2";
|
|
import { db } from "../database/db/index.js";
|
|
import { sshCredentials } from "../database/db/schema.js";
|
|
import { eq, and } from "drizzle-orm";
|
|
import { fileLogger } from "../utils/logger.js";
|
|
|
|
const app = express();
|
|
|
|
app.use(
|
|
cors({
|
|
origin: "*",
|
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
allowedHeaders: ["Content-Type", "Authorization"],
|
|
}),
|
|
);
|
|
app.use(express.json({ limit: "100mb" }));
|
|
app.use(express.urlencoded({ limit: "100mb", extended: true }));
|
|
app.use(express.raw({ limit: "200mb", type: "application/octet-stream" }));
|
|
|
|
interface SSHSession {
|
|
client: SSHClient;
|
|
isConnected: boolean;
|
|
lastActive: number;
|
|
timeout?: NodeJS.Timeout;
|
|
}
|
|
|
|
const sshSessions: Record<string, SSHSession> = {};
|
|
|
|
function cleanupSession(sessionId: string) {
|
|
const session = sshSessions[sessionId];
|
|
if (session) {
|
|
try {
|
|
session.client.end();
|
|
} catch {}
|
|
clearTimeout(session.timeout);
|
|
delete sshSessions[sessionId];
|
|
}
|
|
}
|
|
|
|
function scheduleSessionCleanup(sessionId: string) {
|
|
const session = sshSessions[sessionId];
|
|
if (session) {
|
|
if (session.timeout) clearTimeout(session.timeout);
|
|
}
|
|
}
|
|
|
|
app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|
const {
|
|
sessionId,
|
|
hostId,
|
|
ip,
|
|
port,
|
|
username,
|
|
password,
|
|
sshKey,
|
|
keyPassword,
|
|
authType,
|
|
credentialId,
|
|
userId,
|
|
} = req.body;
|
|
|
|
if (!sessionId || !ip || !username || !port) {
|
|
fileLogger.warn("Missing SSH connection parameters for file manager", {
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hasIp: !!ip,
|
|
hasUsername: !!username,
|
|
hasPort: !!port,
|
|
});
|
|
return res.status(400).json({ error: "Missing SSH connection parameters" });
|
|
}
|
|
|
|
if (sshSessions[sessionId]?.isConnected) {
|
|
cleanupSession(sessionId);
|
|
}
|
|
const client = new SSHClient();
|
|
|
|
let resolvedCredentials = { password, sshKey, keyPassword, authType };
|
|
if (credentialId && hostId && userId) {
|
|
try {
|
|
const credentials = await db
|
|
.select()
|
|
.from(sshCredentials)
|
|
.where(
|
|
and(
|
|
eq(sshCredentials.id, credentialId),
|
|
eq(sshCredentials.userId, userId),
|
|
),
|
|
);
|
|
|
|
if (credentials.length > 0) {
|
|
const credential = credentials[0];
|
|
resolvedCredentials = {
|
|
password: credential.password,
|
|
sshKey: credential.key,
|
|
keyPassword: credential.keyPassword,
|
|
authType: credential.authType,
|
|
};
|
|
} else {
|
|
fileLogger.warn("No credentials found in database for file manager", {
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
credentialId,
|
|
userId,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
fileLogger.warn(
|
|
"Failed to resolve credentials from database for file manager",
|
|
{
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
credentialId,
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
},
|
|
);
|
|
}
|
|
} else if (credentialId && hostId) {
|
|
fileLogger.warn(
|
|
"Missing userId for credential resolution in file manager",
|
|
{
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
credentialId,
|
|
hasUserId: !!userId,
|
|
},
|
|
);
|
|
}
|
|
|
|
const config: any = {
|
|
host: ip,
|
|
port: port || 22,
|
|
username,
|
|
readyTimeout: 0,
|
|
keepaliveInterval: 30000,
|
|
keepaliveCountMax: 0,
|
|
algorithms: {
|
|
kex: [
|
|
"diffie-hellman-group14-sha256",
|
|
"diffie-hellman-group14-sha1",
|
|
"diffie-hellman-group1-sha1",
|
|
"diffie-hellman-group-exchange-sha256",
|
|
"diffie-hellman-group-exchange-sha1",
|
|
"ecdh-sha2-nistp256",
|
|
"ecdh-sha2-nistp384",
|
|
"ecdh-sha2-nistp521",
|
|
],
|
|
cipher: [
|
|
"aes128-ctr",
|
|
"aes192-ctr",
|
|
"aes256-ctr",
|
|
"aes128-gcm@openssh.com",
|
|
"aes256-gcm@openssh.com",
|
|
"aes128-cbc",
|
|
"aes192-cbc",
|
|
"aes256-cbc",
|
|
"3des-cbc",
|
|
],
|
|
hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-md5"],
|
|
compress: ["none", "zlib@openssh.com", "zlib"],
|
|
},
|
|
};
|
|
|
|
if (resolvedCredentials.sshKey && resolvedCredentials.sshKey.trim()) {
|
|
try {
|
|
if (
|
|
!resolvedCredentials.sshKey.includes("-----BEGIN") ||
|
|
!resolvedCredentials.sshKey.includes("-----END")
|
|
) {
|
|
throw new Error("Invalid private key format");
|
|
}
|
|
|
|
const cleanKey = resolvedCredentials.sshKey
|
|
.trim()
|
|
.replace(/\r\n/g, "\n")
|
|
.replace(/\r/g, "\n");
|
|
|
|
config.privateKey = Buffer.from(cleanKey, "utf8");
|
|
|
|
if (resolvedCredentials.keyPassword)
|
|
config.passphrase = resolvedCredentials.keyPassword;
|
|
} catch (keyError) {
|
|
fileLogger.error("SSH key format error for file manager", {
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
error: keyError.message,
|
|
});
|
|
return res.status(400).json({ error: "Invalid SSH key format" });
|
|
}
|
|
} else if (
|
|
resolvedCredentials.password &&
|
|
resolvedCredentials.password.trim()
|
|
) {
|
|
config.password = resolvedCredentials.password;
|
|
} else {
|
|
fileLogger.warn("No authentication method provided for file manager", {
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
});
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Either password or SSH key must be provided" });
|
|
}
|
|
|
|
let responseSent = false;
|
|
|
|
client.on("ready", () => {
|
|
if (responseSent) return;
|
|
responseSent = true;
|
|
sshSessions[sessionId] = {
|
|
client,
|
|
isConnected: true,
|
|
lastActive: Date.now(),
|
|
};
|
|
res.json({ status: "success", message: "SSH connection established" });
|
|
});
|
|
|
|
client.on("error", (err) => {
|
|
if (responseSent) return;
|
|
responseSent = true;
|
|
fileLogger.error("SSH connection failed for file manager", {
|
|
operation: "file_connect",
|
|
sessionId,
|
|
hostId,
|
|
ip,
|
|
port,
|
|
username,
|
|
error: err.message,
|
|
});
|
|
res.status(500).json({ status: "error", message: err.message });
|
|
});
|
|
|
|
client.on("close", () => {
|
|
if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false;
|
|
cleanupSession(sessionId);
|
|
});
|
|
|
|
client.connect(config);
|
|
});
|
|
|
|
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
|
|
const { sessionId } = req.body;
|
|
cleanupSession(sessionId);
|
|
res.json({ status: "success", message: "SSH connection disconnected" });
|
|
});
|
|
|
|
app.get("/ssh/file_manager/ssh/status", (req, res) => {
|
|
const sessionId = req.query.sessionId as string;
|
|
const isConnected = !!sshSessions[sessionId]?.isConnected;
|
|
res.json({ status: "success", connected: isConnected });
|
|
});
|
|
|
|
app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
|
|
const sessionId = req.query.sessionId as string;
|
|
const sshConn = sshSessions[sessionId];
|
|
const sshPath = decodeURIComponent((req.query.path as string) || "/");
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const escapedPath = sshPath.replace(/'/g, "'\"'\"'");
|
|
sshConn.client.exec(`ls -la '${escapedPath}'`, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH listFiles error:", err);
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
|
|
let data = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
return res.status(500).json({ error: `Command failed: ${errorData}` });
|
|
}
|
|
|
|
const lines = data.split("\n").filter((line) => line.trim());
|
|
const files = [];
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const parts = line.split(/\s+/);
|
|
if (parts.length >= 9) {
|
|
const permissions = parts[0];
|
|
const name = parts.slice(8).join(" ");
|
|
const isDirectory = permissions.startsWith("d");
|
|
const isLink = permissions.startsWith("l");
|
|
|
|
if (name === "." || name === "..") continue;
|
|
|
|
files.push({
|
|
name,
|
|
type: isDirectory ? "directory" : isLink ? "link" : "file",
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json(files);
|
|
});
|
|
});
|
|
});
|
|
|
|
app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
|
|
const sessionId = req.query.sessionId as string;
|
|
const sshConn = sshSessions[sessionId];
|
|
const filePath = decodeURIComponent(req.query.path as string);
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!filePath) {
|
|
return res.status(400).json({ error: "File path is required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
|
|
sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH readFile error:", err);
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
|
|
let data = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
return res.status(500).json({ error: `Command failed: ${errorData}` });
|
|
}
|
|
|
|
res.json({ content: data, path: filePath });
|
|
});
|
|
});
|
|
});
|
|
|
|
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
|
|
const { sessionId, path: filePath, content, hostId, userId } = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!filePath) {
|
|
return res.status(400).json({ error: "File path is required" });
|
|
}
|
|
|
|
if (content === undefined) {
|
|
return res.status(400).json({ error: "File content is required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const trySFTP = () => {
|
|
try {
|
|
sshConn.client.sftp((err, sftp) => {
|
|
if (err) {
|
|
fileLogger.warn(
|
|
`SFTP failed, trying fallback method: ${err.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
return;
|
|
}
|
|
|
|
let fileBuffer;
|
|
try {
|
|
if (typeof content === "string") {
|
|
fileBuffer = Buffer.from(content, "utf8");
|
|
} else if (Buffer.isBuffer(content)) {
|
|
fileBuffer = content;
|
|
} else {
|
|
fileBuffer = Buffer.from(content);
|
|
}
|
|
} catch (bufferErr) {
|
|
fileLogger.error("Buffer conversion error:", bufferErr);
|
|
if (!res.headersSent) {
|
|
return res
|
|
.status(500)
|
|
.json({ error: "Invalid file content format" });
|
|
}
|
|
return;
|
|
}
|
|
|
|
const writeStream = sftp.createWriteStream(filePath);
|
|
|
|
let hasError = false;
|
|
let hasFinished = false;
|
|
|
|
writeStream.on("error", (streamErr) => {
|
|
if (hasError || hasFinished) return;
|
|
hasError = true;
|
|
fileLogger.warn(
|
|
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
});
|
|
|
|
writeStream.on("finish", () => {
|
|
if (hasError || hasFinished) return;
|
|
hasFinished = true;
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File written successfully",
|
|
path: filePath,
|
|
toast: { type: "success", message: `File written: ${filePath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
writeStream.on("close", () => {
|
|
if (hasError || hasFinished) return;
|
|
hasFinished = true;
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File written successfully",
|
|
path: filePath,
|
|
toast: { type: "success", message: `File written: ${filePath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
try {
|
|
writeStream.write(fileBuffer);
|
|
writeStream.end();
|
|
} catch (writeErr) {
|
|
if (hasError || hasFinished) return;
|
|
hasError = true;
|
|
fileLogger.warn(
|
|
`SFTP write operation failed, trying fallback method: ${writeErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
}
|
|
});
|
|
} catch (sftpErr) {
|
|
fileLogger.warn(
|
|
`SFTP connection error, trying fallback method: ${sftpErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
}
|
|
};
|
|
|
|
const tryFallbackMethod = () => {
|
|
try {
|
|
const base64Content = Buffer.from(content, "utf8").toString("base64");
|
|
const escapedPath = filePath.replace(/'/g, "'\"'\"'");
|
|
|
|
const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
|
|
|
|
sshConn.client.exec(writeCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("Fallback write command failed:", err);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({
|
|
error: `Write failed: ${err.message}`,
|
|
toast: { type: "error", message: `Write failed: ${err.message}` },
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File written successfully",
|
|
path: filePath,
|
|
toast: {
|
|
type: "success",
|
|
message: `File written: ${filePath}`,
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
fileLogger.error(
|
|
`Fallback write failed with code ${code}: ${errorData}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
error: `Write failed: ${errorData}`,
|
|
toast: { type: "error", message: `Write failed: ${errorData}` },
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("Fallback write stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res
|
|
.status(500)
|
|
.json({ error: `Write stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
} catch (fallbackErr) {
|
|
fileLogger.error("Fallback method failed:", fallbackErr);
|
|
if (!res.headersSent) {
|
|
res
|
|
.status(500)
|
|
.json({ error: `All write methods failed: ${fallbackErr.message}` });
|
|
}
|
|
}
|
|
};
|
|
|
|
trySFTP();
|
|
});
|
|
|
|
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
|
|
const {
|
|
sessionId,
|
|
path: filePath,
|
|
content,
|
|
fileName,
|
|
hostId,
|
|
userId,
|
|
} = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!filePath || !fileName || content === undefined) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "File path, name, and content are required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const fullPath = filePath.endsWith("/")
|
|
? filePath + fileName
|
|
: filePath + "/" + fileName;
|
|
|
|
const trySFTP = () => {
|
|
try {
|
|
sshConn.client.sftp((err, sftp) => {
|
|
if (err) {
|
|
fileLogger.warn(
|
|
`SFTP failed, trying fallback method: ${err.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
return;
|
|
}
|
|
|
|
let fileBuffer;
|
|
try {
|
|
if (typeof content === "string") {
|
|
fileBuffer = Buffer.from(content, "utf8");
|
|
} else if (Buffer.isBuffer(content)) {
|
|
fileBuffer = content;
|
|
} else {
|
|
fileBuffer = Buffer.from(content);
|
|
}
|
|
} catch (bufferErr) {
|
|
fileLogger.error("Buffer conversion error:", bufferErr);
|
|
if (!res.headersSent) {
|
|
return res
|
|
.status(500)
|
|
.json({ error: "Invalid file content format" });
|
|
}
|
|
return;
|
|
}
|
|
|
|
const writeStream = sftp.createWriteStream(fullPath);
|
|
|
|
let hasError = false;
|
|
let hasFinished = false;
|
|
|
|
writeStream.on("error", (streamErr) => {
|
|
if (hasError || hasFinished) return;
|
|
hasError = true;
|
|
fileLogger.warn(
|
|
`SFTP write failed, trying fallback method: ${streamErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
});
|
|
|
|
writeStream.on("finish", () => {
|
|
if (hasError || hasFinished) return;
|
|
hasFinished = true;
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File uploaded successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `File uploaded: ${fullPath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
writeStream.on("close", () => {
|
|
if (hasError || hasFinished) return;
|
|
hasFinished = true;
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File uploaded successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `File uploaded: ${fullPath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
try {
|
|
writeStream.write(fileBuffer);
|
|
writeStream.end();
|
|
} catch (writeErr) {
|
|
if (hasError || hasFinished) return;
|
|
hasError = true;
|
|
fileLogger.warn(
|
|
`SFTP write operation failed, trying fallback method: ${writeErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
}
|
|
});
|
|
} catch (sftpErr) {
|
|
fileLogger.warn(
|
|
`SFTP connection error, trying fallback method: ${sftpErr.message}`,
|
|
);
|
|
tryFallbackMethod();
|
|
}
|
|
};
|
|
|
|
const tryFallbackMethod = () => {
|
|
try {
|
|
const base64Content = Buffer.from(content, "utf8").toString("base64");
|
|
const chunkSize = 1000000;
|
|
const chunks = [];
|
|
|
|
for (let i = 0; i < base64Content.length; i += chunkSize) {
|
|
chunks.push(base64Content.slice(i, i + chunkSize));
|
|
}
|
|
|
|
if (chunks.length === 1) {
|
|
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
|
|
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
|
|
|
const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`;
|
|
|
|
sshConn.client.exec(writeCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("Fallback upload command failed:", err);
|
|
if (!res.headersSent) {
|
|
return res
|
|
.status(500)
|
|
.json({ error: `Upload failed: ${err.message}` });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File uploaded successfully",
|
|
path: fullPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `File uploaded: ${fullPath}`,
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
fileLogger.error(
|
|
`Fallback upload failed with code ${code}: ${errorData}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
error: `Upload failed: ${errorData}`,
|
|
toast: {
|
|
type: "error",
|
|
message: `Upload failed: ${errorData}`,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("Fallback upload stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res
|
|
.status(500)
|
|
.json({ error: `Upload stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'");
|
|
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
|
|
|
let writeCommand = `> '${escapedPath}'`;
|
|
|
|
chunks.forEach((chunk, index) => {
|
|
writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`;
|
|
});
|
|
|
|
writeCommand += ` && echo "SUCCESS"`;
|
|
|
|
sshConn.client.exec(writeCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("Chunked fallback upload failed:", err);
|
|
if (!res.headersSent) {
|
|
return res
|
|
.status(500)
|
|
.json({ error: `Chunked upload failed: ${err.message}` });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File uploaded successfully",
|
|
path: fullPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `File uploaded: ${fullPath}`,
|
|
},
|
|
});
|
|
}
|
|
} else {
|
|
fileLogger.error(
|
|
`Chunked fallback upload failed with code ${code}: ${errorData}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
error: `Chunked upload failed: ${errorData}`,
|
|
toast: {
|
|
type: "error",
|
|
message: `Chunked upload failed: ${errorData}`,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error(
|
|
"Chunked fallback upload stream error:",
|
|
streamErr,
|
|
);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({
|
|
error: `Chunked upload stream error: ${streamErr.message}`,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch (fallbackErr) {
|
|
fileLogger.error("Fallback method failed:", fallbackErr);
|
|
if (!res.headersSent) {
|
|
res
|
|
.status(500)
|
|
.json({ error: `All upload methods failed: ${fallbackErr.message}` });
|
|
}
|
|
}
|
|
};
|
|
|
|
trySFTP();
|
|
});
|
|
|
|
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
|
|
const {
|
|
sessionId,
|
|
path: filePath,
|
|
fileName,
|
|
content = "",
|
|
hostId,
|
|
userId,
|
|
} = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!filePath || !fileName) {
|
|
return res.status(400).json({ error: "File path and name are required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const fullPath = filePath.endsWith("/")
|
|
? filePath + fileName
|
|
: filePath + "/" + fileName;
|
|
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
|
|
|
const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
|
|
|
sshConn.client.exec(createCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH createFile error:", err);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
|
|
if (chunk.toString().includes("Permission denied")) {
|
|
fileLogger.error(`Permission denied creating file: ${fullPath}`);
|
|
if (!res.headersSent) {
|
|
return res.status(403).json({
|
|
error: `Permission denied: Cannot create file ${fullPath}. Check directory permissions.`,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File created successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `File created: ${fullPath}` },
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({
|
|
error: `Command failed: ${errorData}`,
|
|
toast: {
|
|
type: "error",
|
|
message: `File creation failed: ${errorData}`,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "File created successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `File created: ${fullPath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("SSH createFile stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: `Stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
|
|
const { sessionId, path: folderPath, folderName, hostId, userId } = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!folderPath || !folderName) {
|
|
return res.status(400).json({ error: "Folder path and name are required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const fullPath = folderPath.endsWith("/")
|
|
? folderPath + folderName
|
|
: folderPath + "/" + folderName;
|
|
const escapedPath = fullPath.replace(/'/g, "'\"'\"'");
|
|
|
|
const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
|
|
|
sshConn.client.exec(createCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH createFolder error:", err);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
|
|
if (chunk.toString().includes("Permission denied")) {
|
|
fileLogger.error(`Permission denied creating folder: ${fullPath}`);
|
|
if (!res.headersSent) {
|
|
return res.status(403).json({
|
|
error: `Permission denied: Cannot create folder ${fullPath}. Check directory permissions.`,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Folder created successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `Folder created: ${fullPath}` },
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({
|
|
error: `Command failed: ${errorData}`,
|
|
toast: {
|
|
type: "error",
|
|
message: `Folder creation failed: ${errorData}`,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Folder created successfully",
|
|
path: fullPath,
|
|
toast: { type: "success", message: `Folder created: ${fullPath}` },
|
|
});
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("SSH createFolder stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: `Stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
|
|
const { sessionId, path: itemPath, isDirectory, hostId, userId } = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!itemPath) {
|
|
return res.status(400).json({ error: "Item path is required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
const escapedPath = itemPath.replace(/'/g, "'\"'\"'");
|
|
|
|
const deleteCommand = isDirectory
|
|
? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0`
|
|
: `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`;
|
|
|
|
sshConn.client.exec(deleteCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH deleteItem error:", err);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
|
|
if (chunk.toString().includes("Permission denied")) {
|
|
fileLogger.error(`Permission denied deleting: ${itemPath}`);
|
|
if (!res.headersSent) {
|
|
return res.status(403).json({
|
|
error: `Permission denied: Cannot delete ${itemPath}. Check file permissions.`,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Item deleted successfully",
|
|
path: itemPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `${isDirectory ? "Directory" : "File"} deleted: ${itemPath}`,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({
|
|
error: `Command failed: ${errorData}`,
|
|
toast: { type: "error", message: `Delete failed: ${errorData}` },
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Item deleted successfully",
|
|
path: itemPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `${isDirectory ? "Directory" : "File"} deleted: ${itemPath}`,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("SSH deleteItem stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: `Stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
|
|
const { sessionId, oldPath, newName, hostId, userId } = req.body;
|
|
const sshConn = sshSessions[sessionId];
|
|
|
|
if (!sessionId) {
|
|
return res.status(400).json({ error: "Session ID is required" });
|
|
}
|
|
|
|
if (!sshConn?.isConnected) {
|
|
return res.status(400).json({ error: "SSH connection not established" });
|
|
}
|
|
|
|
if (!oldPath || !newName) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: "Old path and new name are required" });
|
|
}
|
|
|
|
sshConn.lastActive = Date.now();
|
|
|
|
const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1);
|
|
const newPath = oldDir + newName;
|
|
const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'");
|
|
const escapedNewPath = newPath.replace(/'/g, "'\"'\"'");
|
|
|
|
const renameCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`;
|
|
|
|
sshConn.client.exec(renameCommand, (err, stream) => {
|
|
if (err) {
|
|
fileLogger.error("SSH renameItem error:", err);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
return;
|
|
}
|
|
|
|
let outputData = "";
|
|
let errorData = "";
|
|
|
|
stream.on("data", (chunk: Buffer) => {
|
|
outputData += chunk.toString();
|
|
});
|
|
|
|
stream.stderr.on("data", (chunk: Buffer) => {
|
|
errorData += chunk.toString();
|
|
|
|
if (chunk.toString().includes("Permission denied")) {
|
|
fileLogger.error(`Permission denied renaming: ${oldPath}`);
|
|
if (!res.headersSent) {
|
|
return res.status(403).json({
|
|
error: `Permission denied: Cannot rename ${oldPath}. Check file permissions.`,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
stream.on("close", (code) => {
|
|
if (outputData.includes("SUCCESS")) {
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Item renamed successfully",
|
|
oldPath,
|
|
newPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `Item renamed: ${oldPath} -> ${newPath}`,
|
|
},
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (code !== 0) {
|
|
fileLogger.error(
|
|
`SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`,
|
|
);
|
|
if (!res.headersSent) {
|
|
return res.status(500).json({
|
|
error: `Command failed: ${errorData}`,
|
|
toast: { type: "error", message: `Rename failed: ${errorData}` },
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!res.headersSent) {
|
|
res.json({
|
|
message: "Item renamed successfully",
|
|
oldPath,
|
|
newPath,
|
|
toast: {
|
|
type: "success",
|
|
message: `Item renamed: ${oldPath} -> ${newPath}`,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
stream.on("error", (streamErr) => {
|
|
fileLogger.error("SSH renameItem stream error:", streamErr);
|
|
if (!res.headersSent) {
|
|
res.status(500).json({ error: `Stream error: ${streamErr.message}` });
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
process.on("SIGINT", () => {
|
|
Object.keys(sshSessions).forEach(cleanupSession);
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on("SIGTERM", () => {
|
|
Object.keys(sshSessions).forEach(cleanupSession);
|
|
process.exit(0);
|
|
});
|
|
|
|
const PORT = 8084;
|
|
app.listen(PORT, () => {
|
|
fileLogger.success("File Manager API server started", {
|
|
operation: "server_start",
|
|
port: PORT,
|
|
});
|
|
});
|