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 = {}; 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, }); });