import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; import { getDb } from "../database/db/index.js"; import { sshCredentials, sshData } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import { fileLogger, sshLogger } from "../utils/logger.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { AuthManager } from "../utils/auth-manager.js"; import type { AuthenticatedRequest } from "../../types/index.js"; import { createSocks5Connection } from "../utils/socks5-helper.js"; function isExecutableFile(permissions: string, fileName: string): boolean { const hasExecutePermission = permissions[3] === "x" || permissions[6] === "x" || permissions[9] === "x"; const scriptExtensions = [ ".sh", ".py", ".pl", ".rb", ".js", ".php", ".bash", ".zsh", ".fish", ]; const hasScriptExtension = scriptExtensions.some((ext) => fileName.toLowerCase().endsWith(ext), ); const executableExtensions = [".bin", ".exe", ".out"]; const hasExecutableExtension = executableExtensions.some((ext) => fileName.toLowerCase().endsWith(ext), ); const hasNoExtension = !fileName.includes(".") && hasExecutePermission; return ( hasExecutePermission && (hasScriptExtension || hasExecutableExtension || hasNoExtension) ); } const app = express(); app.use( cors({ origin: (origin, callback) => { if (!origin) return callback(null, true); const allowedOrigins = [ "http://localhost:5173", "http://localhost:3000", "http://127.0.0.1:5173", "http://127.0.0.1:3000", ]; if (origin.startsWith("https://")) { return callback(null, true); } if (origin.startsWith("http://")) { return callback(null, true); } if (allowedOrigins.includes(origin)) { return callback(null, true); } callback(new Error("Not allowed by CORS")); }, credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: [ "Content-Type", "Authorization", "User-Agent", "X-Electron-App", ], }), ); app.use(cookieParser()); app.use(express.json({ limit: "1gb" })); app.use(express.urlencoded({ limit: "1gb", extended: true })); app.use(express.raw({ limit: "5gb", type: "application/octet-stream" })); const authManager = AuthManager.getInstance(); app.use(authManager.createAuthMiddleware()); async function resolveJumpHost( hostId: number, userId: string, ): Promise { try { const hosts = await SimpleDBOps.select( getDb() .select() .from(sshData) .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))), "ssh_data", userId, ); if (hosts.length === 0) { return null; } const host = hosts[0]; if (host.credentialId) { const credentials = await SimpleDBOps.select( getDb() .select() .from(sshCredentials) .where( and( eq(sshCredentials.id, host.credentialId as number), eq(sshCredentials.userId, userId), ), ), "ssh_credentials", userId, ); if (credentials.length > 0) { const credential = credentials[0]; return { ...host, password: credential.password, key: credential.private_key || credential.privateKey || credential.key, keyPassword: credential.key_password || credential.keyPassword, keyType: credential.key_type || credential.keyType, authType: credential.auth_type || credential.authType, }; } } return host; } catch (error) { fileLogger.error("Failed to resolve jump host", error, { operation: "resolve_jump_host", hostId, userId, }); return null; } } async function createJumpHostChain( jumpHosts: Array<{ hostId: number }>, userId: string, ): Promise { if (!jumpHosts || jumpHosts.length === 0) { return null; } let currentClient: SSHClient | null = null; const clients: SSHClient[] = []; try { for (let i = 0; i < jumpHosts.length; i++) { const jumpHostConfig = await resolveJumpHost(jumpHosts[i].hostId, userId); if (!jumpHostConfig) { fileLogger.error(`Jump host ${i + 1} not found`, undefined, { operation: "jump_host_chain", hostId: jumpHosts[i].hostId, }); clients.forEach((c) => c.end()); return null; } const jumpClient = new SSHClient(); clients.push(jumpClient); const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve(false); }, 30000); jumpClient.on("ready", () => { clearTimeout(timeout); resolve(true); }); jumpClient.on("error", (err) => { clearTimeout(timeout); fileLogger.error(`Jump host ${i + 1} connection failed`, err, { operation: "jump_host_connect", hostId: jumpHostConfig.id, ip: jumpHostConfig.ip, }); resolve(false); }); const connectConfig: any = { host: jumpHostConfig.ip, port: jumpHostConfig.port || 22, username: jumpHostConfig.username, tryKeyboard: true, readyTimeout: 30000, }; if (jumpHostConfig.authType === "password" && jumpHostConfig.password) { connectConfig.password = jumpHostConfig.password; } else if (jumpHostConfig.authType === "key" && jumpHostConfig.key) { const cleanKey = jumpHostConfig.key .trim() .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); connectConfig.privateKey = Buffer.from(cleanKey, "utf8"); if (jumpHostConfig.keyPassword) { connectConfig.passphrase = jumpHostConfig.keyPassword; } } if (currentClient) { currentClient.forwardOut( "127.0.0.1", 0, jumpHostConfig.ip, jumpHostConfig.port || 22, (err, stream) => { if (err) { clearTimeout(timeout); resolve(false); return; } connectConfig.sock = stream; jumpClient.connect(connectConfig); }, ); } else { jumpClient.connect(connectConfig); } }); if (!connected) { clients.forEach((c) => c.end()); return null; } currentClient = jumpClient; } return currentClient; } catch (error) { fileLogger.error("Failed to create jump host chain", error, { operation: "jump_host_chain", }); clients.forEach((c) => c.end()); return null; } } interface SSHSession { client: SSHClient; isConnected: boolean; lastActive: number; timeout?: NodeJS.Timeout; activeOperations: number; } interface PendingTOTPSession { client: SSHClient; finish: (responses: string[]) => void; config: import("ssh2").ConnectConfig; createdAt: number; sessionId: string; hostId?: number; ip?: string; port?: number; username?: string; userId?: string; prompts?: Array<{ prompt: string; echo: boolean }>; totpPromptIndex?: number; resolvedPassword?: string; } const sshSessions: Record = {}; const pendingTOTPSessions: Record = {}; function cleanupSession(sessionId: string) { const session = sshSessions[sessionId]; if (session) { if (session.activeOperations > 0) { fileLogger.warn( `Deferring session cleanup for ${sessionId} - ${session.activeOperations} active operations`, { operation: "cleanup_deferred", sessionId, activeOperations: session.activeOperations, }, ); scheduleSessionCleanup(sessionId); return; } try { session.client.end(); } catch (error) {} clearTimeout(session.timeout); delete sshSessions[sessionId]; } } function scheduleSessionCleanup(sessionId: string) { const session = sshSessions[sessionId]; if (session) { if (session.timeout) clearTimeout(session.timeout); session.timeout = setTimeout( () => { cleanupSession(sessionId); }, 30 * 60 * 1000, ); } } function getMimeType(fileName: string): string { const ext = fileName.split(".").pop()?.toLowerCase(); const mimeTypes: Record = { txt: "text/plain", json: "application/json", js: "text/javascript", html: "text/html", css: "text/css", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", pdf: "application/pdf", zip: "application/zip", tar: "application/x-tar", gz: "application/gzip", }; return mimeTypes[ext || ""] || "application/octet-stream"; } app.post("/ssh/file_manager/ssh/connect", async (req, res) => { const { sessionId, hostId, ip, port, username, password, sshKey, keyPassword, authType, credentialId, userProvidedPassword, forceKeyboardInteractive, jumpHosts, useSocks5, socks5Host, socks5Port, socks5Username, socks5Password, socks5ProxyChain, } = req.body; const userId = (req as AuthenticatedRequest).userId; if (!userId) { fileLogger.error("SSH connection rejected: no authenticated user", { operation: "file_connect_auth", sessionId, }); return res.status(401).json({ error: "Authentication required" }); } 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 SimpleDBOps.select( getDb() .select() .from(sshCredentials) .where( and( eq(sshCredentials.id, credentialId), eq(sshCredentials.userId, userId), ), ), "ssh_credentials", userId, ); if (credentials.length > 0) { const credential = credentials[0]; resolvedCredentials = { password: credential.password, sshKey: credential.private_key || credential.privateKey || credential.key, keyPassword: credential.key_password || credential.keyPassword, authType: credential.auth_type || credential.authType, }; } else { fileLogger.warn(`No credentials found for host ${hostId}`, { operation: "ssh_credentials", hostId, credentialId, userId, }); } } catch (error) { fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, { operation: "ssh_credentials", 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: "ssh_credentials", hostId, credentialId, hasUserId: !!userId, }, ); } const config: Record = { host: ip, port, username, tryKeyboard: true, keepaliveInterval: 30000, keepaliveCountMax: 3, readyTimeout: 60000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, env: { TERM: "xterm-256color", LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8", LC_CTYPE: "en_US.UTF-8", LC_MESSAGES: "en_US.UTF-8", LC_MONETARY: "en_US.UTF-8", LC_NUMERIC: "en_US.UTF-8", LC_TIME: "en_US.UTF-8", LC_COLLATE: "en_US.UTF-8", COLORTERM: "truecolor", }, algorithms: { kex: [ "curve25519-sha256", "curve25519-sha256@libssh.org", "ecdh-sha2-nistp521", "ecdh-sha2-nistp384", "ecdh-sha2-nistp256", "diffie-hellman-group-exchange-sha256", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1", "diffie-hellman-group-exchange-sha1", "diffie-hellman-group1-sha1", ], serverHostKey: [ "ssh-ed25519", "ecdsa-sha2-nistp521", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp256", "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa", "ssh-dss", ], cipher: [ "chacha20-poly1305@openssh.com", "aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr", "aes256-cbc", "aes192-cbc", "aes128-cbc", "3des-cbc", ], hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", "hmac-sha2-512", "hmac-sha2-256", "hmac-sha1", "hmac-md5", ], compress: ["none", "zlib@openssh.com", "zlib"], }, }; if ( resolvedCredentials.authType === "key" && 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.authType === "password") { if (!resolvedCredentials.password || !resolvedCredentials.password.trim()) { return res .status(400) .json({ error: "Password required for password authentication" }); } if (!forceKeyboardInteractive) { config.password = resolvedCredentials.password; } } else if (resolvedCredentials.authType === "none") { } else { fileLogger.warn( "No valid authentication method provided for file manager", { operation: "file_connect", sessionId, hostId, authType: resolvedCredentials.authType, hasPassword: !!resolvedCredentials.password, hasKey: !!resolvedCredentials.sshKey, }, ); 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(), activeOperations: 0, }; scheduleSessionCleanup(sessionId); res.json({ status: "success", message: "SSH connection established" }); if (hostId && userId) { (async () => { try { const hosts = await SimpleDBOps.select( getDb() .select() .from(sshData) .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))), "ssh_data", userId, ); const hostName = hosts.length > 0 && hosts[0].name ? hosts[0].name : `${username}@${ip}:${port}`; const authManager = AuthManager.getInstance(); await axios.post( "http://localhost:30006/activity/log", { type: "file_manager", hostId, hostName, }, { headers: { Authorization: `Bearer ${await authManager.generateJWTToken(userId)}`, }, }, ); } catch (error) { fileLogger.warn("Failed to log file manager activity", { operation: "activity_log_error", userId, hostId, error: error instanceof Error ? error.message : "Unknown error", }); } })(); } }); 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, }); if ( resolvedCredentials.authType === "none" && (err.message.includes("authentication") || err.message.includes("All configured authentication methods failed")) ) { res.json({ status: "auth_required", reason: "no_keyboard", }); } else { res.status(500).json({ status: "error", message: err.message }); } }); client.on("close", () => { if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false; cleanupSession(sessionId); }); let keyboardInteractiveResponded = false; client.on( "keyboard-interactive", ( name: string, instructions: string, instructionsLang: string, prompts: Array<{ prompt: string; echo: boolean }>, finish: (responses: string[]) => void, ) => { const promptTexts = prompts.map((p) => p.prompt); const totpPromptIndex = prompts.findIndex((p) => /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test( p.prompt, ), ); if (totpPromptIndex !== -1) { if (responseSent) { const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; } return ""; }); finish(responses); return; } responseSent = true; if (pendingTOTPSessions[sessionId]) { const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; } return ""; }); finish(responses); return; } keyboardInteractiveResponded = true; pendingTOTPSessions[sessionId] = { client, finish, config, createdAt: Date.now(), sessionId, hostId, ip, port, username, userId, prompts, totpPromptIndex, resolvedPassword: resolvedCredentials.password, }; res.json({ requires_totp: true, sessionId, prompt: prompts[totpPromptIndex].prompt, }); } else { const hasStoredPassword = resolvedCredentials.password && resolvedCredentials.authType !== "none"; const passwordPromptIndex = prompts.findIndex((p) => /password/i.test(p.prompt), ); if ( resolvedCredentials.authType === "none" && passwordPromptIndex !== -1 ) { if (responseSent) return; responseSent = true; client.end(); res.json({ status: "auth_required", reason: "no_keyboard", }); return; } if (!hasStoredPassword && passwordPromptIndex !== -1) { if (responseSent) { const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; } return ""; }); finish(responses); return; } responseSent = true; if (pendingTOTPSessions[sessionId]) { const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; } return ""; }); finish(responses); return; } keyboardInteractiveResponded = true; pendingTOTPSessions[sessionId] = { client, finish, config, createdAt: Date.now(), sessionId, hostId, ip, port, username, userId, prompts, totpPromptIndex: passwordPromptIndex, resolvedPassword: resolvedCredentials.password, }; res.json({ requires_totp: true, sessionId, prompt: prompts[passwordPromptIndex].prompt, isPassword: true, }); return; } const responses = prompts.map((p) => { if (/password/i.test(p.prompt) && resolvedCredentials.password) { return resolvedCredentials.password; } return ""; }); finish(responses); } }, ); fileLogger.info("SFTP connection request received", { operation: "sftp_connect_request", sessionId, hostId, ip, port, useSocks5, socks5Host, socks5Port, hasSocks5ProxyChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0), proxyChainLength: socks5ProxyChain ? (socks5ProxyChain as any).length : 0, }); // Check if SOCKS5 proxy is enabled (either single proxy or chain) if (useSocks5 && (socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0))) { fileLogger.info("SOCKS5 enabled for SFTP, creating connection", { operation: "sftp_socks5_enabled", sessionId, socks5Host, socks5Port, hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0), }); try { const socks5Socket = await createSocks5Connection( ip, port, { useSocks5, socks5Host, socks5Port, socks5Username, socks5Password, socks5ProxyChain: socks5ProxyChain as any, }, ); if (socks5Socket) { fileLogger.info("SOCKS5 socket created for SFTP", { operation: "sftp_socks5_socket_ready", sessionId, }); config.sock = socks5Socket; client.connect(config); return; } else { fileLogger.error("SOCKS5 socket is null for SFTP", undefined, { operation: "sftp_socks5_socket_null", sessionId, }); } } catch (socks5Error) { fileLogger.error("SOCKS5 connection failed", socks5Error, { operation: "socks5_connect", sessionId, hostId, proxyHost: socks5Host, proxyPort: socks5Port || 1080, }); return res.status(500).json({ error: "SOCKS5 proxy connection failed: " + (socks5Error instanceof Error ? socks5Error.message : "Unknown error"), }); } } else { fileLogger.info("SOCKS5 NOT enabled for SFTP connection", { operation: "sftp_no_socks5", sessionId, useSocks5, socks5Host, hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0), }); } if (jumpHosts && jumpHosts.length > 0 && userId) { try { const jumpClient = await createJumpHostChain(jumpHosts, userId); if (!jumpClient) { fileLogger.error("Failed to establish jump host chain", { operation: "file_jump_chain", sessionId, hostId, }); return res .status(500) .json({ error: "Failed to connect through jump hosts" }); } jumpClient.forwardOut("127.0.0.1", 0, ip, port, (err, stream) => { if (err) { fileLogger.error("Failed to forward through jump host", err, { operation: "file_jump_forward", sessionId, hostId, ip, port, }); jumpClient.end(); return res.status(500).json({ error: "Failed to forward through jump host: " + err.message, }); } config.sock = stream; client.connect(config); }); } catch (error) { fileLogger.error("Jump host error", error, { operation: "file_jump_host", sessionId, hostId, }); return res .status(500) .json({ error: "Failed to connect through jump hosts" }); } } else { client.connect(config); } }); app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { const { sessionId, totpCode } = req.body; const userId = (req as AuthenticatedRequest).userId; if (!userId) { fileLogger.error("TOTP verification rejected: no authenticated user", { operation: "file_totp_auth", sessionId, }); return res.status(401).json({ error: "Authentication required" }); } if (!sessionId || !totpCode) { return res.status(400).json({ error: "Session ID and TOTP code required" }); } const session = pendingTOTPSessions[sessionId]; if (!session) { fileLogger.warn("TOTP session not found or expired", { operation: "file_totp_verify", sessionId, userId, availableSessions: Object.keys(pendingTOTPSessions), }); return res .status(404) .json({ error: "TOTP session expired. Please reconnect." }); } if (Date.now() - session.createdAt > 180000) { delete pendingTOTPSessions[sessionId]; try { session.client.end(); } catch (error) { sshLogger.debug("Operation failed, continuing", { error }); } fileLogger.warn("TOTP session timeout before code submission", { operation: "file_totp_verify", sessionId, userId, age: Date.now() - session.createdAt, }); return res .status(408) .json({ error: "TOTP session timeout. Please reconnect." }); } const responses = (session.prompts || []).map((p, index) => { if (index === session.totpPromptIndex) { return totpCode; } if (/password/i.test(p.prompt) && session.resolvedPassword) { return session.resolvedPassword; } return ""; }); let responseSent = false; let responseTimeout: NodeJS.Timeout; session.client.once("ready", () => { if (responseSent) return; responseSent = true; clearTimeout(responseTimeout); delete pendingTOTPSessions[sessionId]; setTimeout(() => { sshSessions[sessionId] = { client: session.client, isConnected: true, lastActive: Date.now(), activeOperations: 0, }; scheduleSessionCleanup(sessionId); res.json({ status: "success", message: "TOTP verified, SSH connection established", }); if (session.hostId && session.userId) { (async () => { try { const hosts = await SimpleDBOps.select( getDb() .select() .from(sshData) .where( and( eq(sshData.id, session.hostId!), eq(sshData.userId, session.userId!), ), ), "ssh_data", session.userId!, ); const hostName = hosts.length > 0 && hosts[0].name ? hosts[0].name : `${session.username}@${session.ip}:${session.port}`; const authManager = AuthManager.getInstance(); await axios.post( "http://localhost:30006/activity/log", { type: "file_manager", hostId: session.hostId, hostName, }, { headers: { Authorization: `Bearer ${await authManager.generateJWTToken(session.userId!)}`, }, }, ); } catch (error) { fileLogger.warn("Failed to log file manager activity (TOTP)", { operation: "activity_log_error", userId: session.userId, hostId: session.hostId, error: error instanceof Error ? error.message : "Unknown error", }); } })(); } }, 200); }); session.client.once("error", (err) => { if (responseSent) return; responseSent = true; clearTimeout(responseTimeout); delete pendingTOTPSessions[sessionId]; fileLogger.error("TOTP verification failed", { operation: "file_totp_verify", sessionId, userId, error: err.message, }); res.status(401).json({ status: "error", message: "Invalid TOTP code" }); }); responseTimeout = setTimeout(() => { if (!responseSent) { responseSent = true; delete pendingTOTPSessions[sessionId]; fileLogger.warn("TOTP verification timeout", { operation: "file_totp_verify", sessionId, userId, }); res.status(408).json({ error: "TOTP verification timeout" }); } }, 60000); session.finish(responses); }); 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.post("/ssh/file_manager/ssh/keepalive", (req, res) => { const { sessionId } = req.body; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); } const session = sshSessions[sessionId]; if (!session || !session.isConnected) { return res.status(400).json({ error: "SSH session not found or not connected", connected: false, }); } session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); res.json({ status: "success", connected: true, message: "Session keepalive successful", lastActive: session.lastActive, }); }); 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(); sshConn.activeOperations++; const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => { if (err) { sshConn.activeOperations--; 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) => { sshConn.activeOperations--; 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 owner = parts[2]; const group = parts[3]; const size = parseInt(parts[4], 10); let dateStr = ""; const nameStartIndex = 8; if (parts[5] && parts[6] && parts[7]) { dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`; } const name = parts.slice(nameStartIndex).join(" "); const isDirectory = permissions.startsWith("d"); const isLink = permissions.startsWith("l"); if (name === "." || name === "..") continue; let actualName = name; let linkTarget = undefined; if (isLink && name.includes(" -> ")) { const linkParts = name.split(" -> "); actualName = linkParts[0]; linkTarget = linkParts[1]; } files.push({ name: actualName, type: isDirectory ? "directory" : isLink ? "link" : "file", size: isDirectory ? undefined : size, modified: dateStr, permissions, owner, group, linkTarget, path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${actualName}`, executable: !isDirectory && !isLink ? isExecutableFile(permissions, actualName) : false, }); } } res.json({ files, path: sshPath }); }); }); }); app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; const linkPath = 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 (!linkPath) { return res.status(400).json({ error: "Link path is required" }); } sshConn.lastActive = Date.now(); const escapedPath = linkPath.replace(/'/g, "'\"'\"'"); const command = `stat -L -c "%F" '${escapedPath}' && readlink -f '${escapedPath}'`; sshConn.client.exec(command, (err, stream) => { if (err) { fileLogger.error("SSH identifySymlink 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 identifySymlink command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, ); return res.status(500).json({ error: `Command failed: ${errorData}` }); } const [fileType, target] = data.trim().split("\n"); res.json({ path: linkPath, target: target, type: fileType.toLowerCase().includes("directory") ? "directory" : "file", }); }); stream.on("error", (streamErr) => { fileLogger.error("SSH identifySymlink stream error:", streamErr); if (!res.headersSent) { res.status(500).json({ error: `Stream error: ${streamErr.message}` }); } }); }); }); 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 MAX_READ_SIZE = 500 * 1024 * 1024; const escapedPath = filePath.replace(/'/g, "'\"'\"'"); sshConn.client.exec( `stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`, (sizeErr, sizeStream) => { if (sizeErr) { fileLogger.error("SSH file size check error:", sizeErr); return res.status(500).json({ error: sizeErr.message }); } let sizeData = ""; let sizeErrorData = ""; sizeStream.on("data", (chunk: Buffer) => { sizeData += chunk.toString(); }); sizeStream.stderr.on("data", (chunk: Buffer) => { sizeErrorData += chunk.toString(); }); sizeStream.on("close", (sizeCode) => { if (sizeCode !== 0) { const errorLower = sizeErrorData.toLowerCase(); const isFileNotFound = errorLower.includes("no such file or directory") || errorLower.includes("cannot access") || errorLower.includes("not found") || errorLower.includes("resource not found"); fileLogger.error(`File size check failed: ${sizeErrorData}`); return res.status(isFileNotFound ? 404 : 500).json({ error: `Cannot check file size: ${sizeErrorData}`, fileNotFound: isFileNotFound, }); } const fileSize = parseInt(sizeData.trim(), 10); if (isNaN(fileSize)) { fileLogger.error("Invalid file size response:", sizeData); return res.status(500).json({ error: "Cannot determine file size" }); } if (fileSize > MAX_READ_SIZE) { fileLogger.warn("File too large for reading", { operation: "file_read", sessionId, filePath, fileSize, maxSize: MAX_READ_SIZE, }); return res.status(400).json({ error: `File too large to open in editor. Maximum size is ${MAX_READ_SIZE / 1024 / 1024}MB, file is ${(fileSize / 1024 / 1024).toFixed(2)}MB. Use download instead.`, fileSize, maxSize: MAX_READ_SIZE, tooLarge: true, }); } 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()}`, ); const isFileNotFound = errorData.includes("No such file or directory") || errorData.includes("cannot access") || errorData.includes("not found"); return res.status(isFileNotFound ? 404 : 500).json({ error: `Command failed: ${errorData}`, fileNotFound: isFileNotFound, }); } res.json({ content: data, path: filePath }); }); }); }); }, ); }); app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { const { sessionId, path: filePath, content } = 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 } = 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 contentSize = typeof content === "string" ? Buffer.byteLength(content, "utf8") : content.length; 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}`, { operation: "file_upload", sessionId, fileName, fileSize: contentSize, error: 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 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 escapedPath = fullPath.replace(/'/g, "'\"'\"'"); let writeCommand = `> '${escapedPath}'`; chunks.forEach((chunk) => { 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 } = 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 } = 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 } = 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 } = 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}` }); } }); }); }); app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { const { sessionId, oldPath, newPath } = 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 || !newPath) { return res .status(400) .json({ error: "Old path and new path are required" }); } sshConn.lastActive = Date.now(); const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'"); const escapedNewPath = newPath.replace(/'/g, "'\"'\"'"); const moveCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`; const commandTimeout = setTimeout(() => { if (!res.headersSent) { res.status(408).json({ error: "Move operation timed out. SSH connection may be unstable.", toast: { type: "error", message: "Move operation timed out. SSH connection may be unstable.", }, }); } }, 60000); sshConn.client.exec(moveCommand, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH moveItem 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 moving: ${oldPath}`); if (!res.headersSent) { return res.status(403).json({ error: `Permission denied: Cannot move ${oldPath}. Check file permissions.`, toast: { type: "error", message: `Permission denied: Cannot move ${oldPath}. Check file permissions.`, }, }); } return; } }); stream.on("close", (code) => { clearTimeout(commandTimeout); if (outputData.includes("SUCCESS")) { if (!res.headersSent) { res.json({ message: "Item moved successfully", oldPath, newPath, toast: { type: "success", message: `Item moved: ${oldPath} -> ${newPath}`, }, }); } return; } if (code !== 0) { fileLogger.error( `SSH moveItem 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: `Move failed: ${errorData}` }, }); } return; } if (!res.headersSent) { res.json({ message: "Item moved successfully", oldPath, newPath, toast: { type: "success", message: `Item moved: ${oldPath} -> ${newPath}`, }, }); } }); stream.on("error", (streamErr) => { clearTimeout(commandTimeout); fileLogger.error("SSH moveItem stream error:", streamErr); if (!res.headersSent) { res.status(500).json({ error: `Stream error: ${streamErr.message}` }); } }); }); }); app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { const { sessionId, path: filePath, hostId, userId } = req.body; if (!sessionId || !filePath) { fileLogger.warn("Missing download parameters", { operation: "file_download", sessionId, hasFilePath: !!filePath, }); return res.status(400).json({ error: "Missing download parameters" }); } const sshConn = sshSessions[sessionId]; if (!sshConn || !sshConn.isConnected) { fileLogger.warn("SSH session not found or not connected for download", { operation: "file_download", sessionId, isConnected: sshConn?.isConnected, }); return res .status(400) .json({ error: "SSH session not found or not connected" }); } sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); sshConn.client.sftp((err, sftp) => { if (err) { fileLogger.error("SFTP connection failed for download:", err); return res.status(500).json({ error: "SFTP connection failed" }); } sftp.stat(filePath, (statErr, stats) => { if (statErr) { fileLogger.error("File stat failed for download:", statErr); return res .status(500) .json({ error: `Cannot access file: ${statErr.message}` }); } if (!stats.isFile()) { fileLogger.warn("Attempted to download non-file", { operation: "file_download", sessionId, filePath, isFile: stats.isFile(), isDirectory: stats.isDirectory(), }); return res .status(400) .json({ error: "Cannot download directories or special files" }); } const MAX_FILE_SIZE = 5 * 1024 * 1024 * 1024; if (stats.size > MAX_FILE_SIZE) { fileLogger.warn("File too large for download", { operation: "file_download", sessionId, filePath, fileSize: stats.size, maxSize: MAX_FILE_SIZE, }); return res.status(400).json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB, file is ${(stats.size / 1024 / 1024).toFixed(2)}MB`, }); } sftp.readFile(filePath, (readErr, data) => { if (readErr) { fileLogger.error("File read failed for download:", readErr); return res .status(500) .json({ error: `Failed to read file: ${readErr.message}` }); } const base64Content = data.toString("base64"); const fileName = filePath.split("/").pop() || "download"; fileLogger.success("File downloaded successfully", { operation: "file_download", sessionId, filePath, fileName, fileSize: stats.size, hostId, userId, }); res.json({ content: base64Content, fileName: fileName, size: stats.size, mimeType: getMimeType(fileName), path: filePath, }); }); }); }); }); app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { const { sessionId, sourcePath, targetDir, hostId, userId } = req.body; if (!sessionId || !sourcePath || !targetDir) { return res.status(400).json({ error: "Missing required parameters" }); } const sshConn = sshSessions[sessionId]; if (!sshConn || !sshConn.isConnected) { return res .status(400) .json({ error: "SSH session not found or not connected" }); } sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const sourceName = sourcePath.split("/").pop() || "copied_item"; const timestamp = Date.now().toString().slice(-8); const uniqueName = `${sourceName}_copy_${timestamp}`; const targetPath = `${targetDir}/${uniqueName}`; const escapedSource = sourcePath.replace(/'/g, "'\"'\"'"); const escapedTarget = targetPath.replace(/'/g, "'\"'\"'"); const copyCommand = `cp '${escapedSource}' '${escapedTarget}' && echo "COPY_SUCCESS"`; const commandTimeout = setTimeout(() => { fileLogger.error("Copy command timed out after 60 seconds", { sourcePath, targetPath, command: copyCommand, }); if (!res.headersSent) { res.status(500).json({ error: "Copy operation timed out", toast: { type: "error", message: "Copy operation timed out. SSH connection may be unstable.", }, }); } }, 60000); sshConn.client.exec(copyCommand, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH copyItem error:", err); if (!res.headersSent) { return res.status(500).json({ error: err.message }); } return; } let errorData = ""; let stdoutData = ""; stream.on("data", (data: Buffer) => { const output = data.toString(); stdoutData += output; stream.stderr.on("data", (data: Buffer) => { const output = data.toString(); errorData += output; }); stream.on("close", (code) => { clearTimeout(commandTimeout); if (code !== 0) { const fullErrorInfo = errorData || stdoutData || "No error message available"; fileLogger.error(`SSH copyItem command failed with code ${code}`, { operation: "file_copy_failed", sessionId, sourcePath, targetPath, command: copyCommand, exitCode: code, errorData, stdoutData, fullErrorInfo, }); if (!res.headersSent) { return res.status(500).json({ error: `Copy failed: ${fullErrorInfo}`, toast: { type: "error", message: `Copy failed: ${fullErrorInfo}`, }, debug: { sourcePath, targetPath, exitCode: code, command: copyCommand, }, }); } return; } const copySuccessful = stdoutData.includes("COPY_SUCCESS") || code === 0; if (copySuccessful) { fileLogger.success("Item copied successfully", { operation: "file_copy", sessionId, sourcePath, targetPath, uniqueName, hostId, userId, }); if (!res.headersSent) { res.json({ message: "Item copied successfully", sourcePath, targetPath, uniqueName, toast: { type: "success", message: `Successfully copied to: ${uniqueName}`, }, }); } } else { fileLogger.warn("Copy completed but without success confirmation", { operation: "file_copy_uncertain", sessionId, sourcePath, targetPath, code, stdoutData: stdoutData.substring(0, 200), }); if (!res.headersSent) { res.json({ message: "Copy may have completed", sourcePath, targetPath, uniqueName, toast: { type: "warning", message: `Copy completed but verification uncertain for: ${uniqueName}`, }, }); } } }); stream.on("error", (streamErr) => { clearTimeout(commandTimeout); fileLogger.error("SSH copyItem stream error:", streamErr); if (!res.headersSent) { res.status(500).json({ error: `Stream error: ${streamErr.message}` }); } }); }); }); }); app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { const { sessionId, filePath } = req.body; const sshConn = sshSessions[sessionId]; if (!sshConn || !sshConn.isConnected) { fileLogger.error( "SSH connection not found or not connected for executeFile", { operation: "execute_file", sessionId, hasConnection: !!sshConn, isConnected: sshConn?.isConnected, }, ); return res.status(400).json({ error: "SSH connection not available" }); } if (!filePath) { return res.status(400).json({ error: "File path is required" }); } const escapedPath = filePath.replace(/'/g, "'\"'\"'"); const checkCommand = `test -x '${escapedPath}' && echo "EXECUTABLE" || echo "NOT_EXECUTABLE"`; sshConn.client.exec(checkCommand, (checkErr, checkStream) => { if (checkErr) { fileLogger.error("SSH executeFile check error:", checkErr); return res .status(500) .json({ error: "Failed to check file executability" }); } let checkResult = ""; checkStream.on("data", (data) => { checkResult += data.toString(); }); checkStream.on("close", () => { if (!checkResult.includes("EXECUTABLE")) { return res.status(400).json({ error: "File is not executable" }); } const executeCommand = `cd "$(dirname '${escapedPath}')" && '${escapedPath}' 2>&1; echo "EXIT_CODE:$?"`; sshConn.client.exec(executeCommand, (err, stream) => { if (err) { fileLogger.error("SSH executeFile error:", err); return res.status(500).json({ error: "Failed to execute file" }); } let output = ""; let errorOutput = ""; stream.on("data", (data) => { output += data.toString(); }); stream.stderr.on("data", (data) => { errorOutput += data.toString(); }); stream.on("close", (code) => { const exitCodeMatch = output.match(/EXIT_CODE:(\d+)$/); const actualExitCode = exitCodeMatch ? parseInt(exitCodeMatch[1]) : code; const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim(); fileLogger.info("File execution completed", { operation: "execute_file", sessionId, filePath, exitCode: actualExitCode, outputLength: cleanOutput.length, errorLength: errorOutput.length, }); res.json({ success: true, exitCode: actualExitCode, output: cleanOutput, error: errorOutput, timestamp: new Date().toISOString(), }); }); stream.on("error", (streamErr) => { fileLogger.error("SSH executeFile stream error:", streamErr); if (!res.headersSent) { res.status(500).json({ error: "Execution stream error" }); } }); }); }); }); }); app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => { const { sessionId, path, permissions } = req.body; const sshConn = sshSessions[sessionId]; if (!sshConn || !sshConn.isConnected) { fileLogger.error( "SSH connection not found or not connected for changePermissions", { operation: "change_permissions", sessionId, hasConnection: !!sshConn, isConnected: sshConn?.isConnected, }, ); return res.status(400).json({ error: "SSH connection not available" }); } if (!path) { return res.status(400).json({ error: "File path is required" }); } if (!permissions || !/^\d{3,4}$/.test(permissions)) { return res.status(400).json({ error: "Valid permissions required (e.g., 755, 644)", }); } sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const octalPerms = permissions.slice(-3); const escapedPath = path.replace(/'/g, "'\"'\"'"); const command = `chmod ${octalPerms} '${escapedPath}' && echo "SUCCESS"`; fileLogger.info("Changing file permissions", { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); const commandTimeout = setTimeout(() => { if (!res.headersSent) { fileLogger.error("changePermissions command timeout", { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); res.status(408).json({ error: "Permission change timed out. SSH connection may be unstable.", }); } }, 10000); sshConn.client.exec(command, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH changePermissions exec error:", err, { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); if (!res.headersSent) { return res.status(500).json({ error: "Failed to change permissions" }); } return; } let outputData = ""; let errorOutput = ""; stream.on("data", (chunk: Buffer) => { outputData += chunk.toString(); }); stream.stderr.on("data", (data: Buffer) => { errorOutput += data.toString(); }); stream.on("close", (code) => { clearTimeout(commandTimeout); if (outputData.includes("SUCCESS")) { fileLogger.success("File permissions changed successfully", { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); if (!res.headersSent) { res.json({ success: true, message: "Permissions changed successfully", }); } return; } if (code !== 0) { fileLogger.error("chmod command failed", { operation: "change_permissions", sessionId, path, permissions: octalPerms, exitCode: code, error: errorOutput, }); if (!res.headersSent) { return res.status(500).json({ error: errorOutput || "Failed to change permissions", }); } return; } fileLogger.success("File permissions changed successfully", { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); if (!res.headersSent) { res.json({ success: true, message: "Permissions changed successfully", }); } }); stream.on("error", (streamErr) => { clearTimeout(commandTimeout); fileLogger.error("SSH changePermissions stream error:", streamErr, { operation: "change_permissions", sessionId, path, permissions: octalPerms, }); if (!res.headersSent) { res .status(500) .json({ error: "Stream error while changing permissions" }); } }); }); }); // Route: Extract archive file (requires JWT) // POST /ssh/file_manager/ssh/extractArchive app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { const { sessionId, archivePath, extractPath } = req.body; if (!sessionId || !archivePath) { return res.status(400).json({ error: "Missing required parameters" }); } const session = sshSessions[sessionId]; if (!session || !session.isConnected) { return res.status(400).json({ error: "SSH session not connected" }); } session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const fileName = archivePath.split("/").pop() || ""; const fileExt = fileName.toLowerCase(); let extractCommand = ""; const targetPath = extractPath || archivePath.substring(0, archivePath.lastIndexOf("/")); if (fileExt.endsWith(".tar.gz") || fileExt.endsWith(".tgz")) { extractCommand = `tar -xzf "${archivePath}" -C "${targetPath}"`; } else if (fileExt.endsWith(".tar.bz2") || fileExt.endsWith(".tbz2")) { extractCommand = `tar -xjf "${archivePath}" -C "${targetPath}"`; } else if (fileExt.endsWith(".tar.xz")) { extractCommand = `tar -xJf "${archivePath}" -C "${targetPath}"`; } else if (fileExt.endsWith(".tar")) { extractCommand = `tar -xf "${archivePath}" -C "${targetPath}"`; } else if (fileExt.endsWith(".zip")) { extractCommand = `unzip -o "${archivePath}" -d "${targetPath}"`; } else if (fileExt.endsWith(".gz") && !fileExt.endsWith(".tar.gz")) { extractCommand = `gunzip -c "${archivePath}" > "${archivePath.replace(/\.gz$/, "")}"`; } else if (fileExt.endsWith(".bz2") && !fileExt.endsWith(".tar.bz2")) { extractCommand = `bunzip2 -k "${archivePath}"`; } else if (fileExt.endsWith(".xz") && !fileExt.endsWith(".tar.xz")) { extractCommand = `unxz -k "${archivePath}"`; } else if (fileExt.endsWith(".7z")) { extractCommand = `7z x "${archivePath}" -o"${targetPath}"`; } else if (fileExt.endsWith(".rar")) { extractCommand = `unrar x "${archivePath}" "${targetPath}/"`; } else { return res.status(400).json({ error: "Unsupported archive format" }); } fileLogger.info("Extracting archive", { operation: "extract_archive", sessionId, archivePath, extractPath: targetPath, command: extractCommand, }); session.client.exec(extractCommand, (err, stream) => { if (err) { fileLogger.error("SSH exec error during extract:", err, { operation: "extract_archive", sessionId, archivePath, }); return res .status(500) .json({ error: "Failed to execute extract command" }); } let errorOutput = ""; stream.on("data", (data: Buffer) => { fileLogger.debug("Extract stdout", { operation: "extract_archive", sessionId, output: data.toString(), }); }); stream.stderr.on("data", (data: Buffer) => { errorOutput += data.toString(); fileLogger.debug("Extract stderr", { operation: "extract_archive", sessionId, error: data.toString(), }); }); stream.on("close", (code: number) => { if (code !== 0) { fileLogger.error("Extract command failed", { operation: "extract_archive", sessionId, archivePath, exitCode: code, error: errorOutput, }); let friendlyError = errorOutput || "Failed to extract archive"; if ( errorOutput.includes("command not found") || errorOutput.includes("not found") ) { let missingCmd = ""; let installHint = ""; if (fileExt.endsWith(".zip")) { missingCmd = "unzip"; installHint = "apt install unzip / yum install unzip / brew install unzip"; } else if ( fileExt.endsWith(".tar.gz") || fileExt.endsWith(".tgz") || fileExt.endsWith(".tar.bz2") || fileExt.endsWith(".tbz2") || fileExt.endsWith(".tar.xz") || fileExt.endsWith(".tar") ) { missingCmd = "tar"; installHint = "Usually pre-installed on Linux/Unix systems"; } else if (fileExt.endsWith(".gz")) { missingCmd = "gunzip"; installHint = "apt install gzip / yum install gzip / Usually pre-installed"; } else if (fileExt.endsWith(".bz2")) { missingCmd = "bunzip2"; installHint = "apt install bzip2 / yum install bzip2 / brew install bzip2"; } else if (fileExt.endsWith(".xz")) { missingCmd = "unxz"; installHint = "apt install xz-utils / yum install xz / brew install xz"; } else if (fileExt.endsWith(".7z")) { missingCmd = "7z"; installHint = "apt install p7zip-full / yum install p7zip / brew install p7zip"; } else if (fileExt.endsWith(".rar")) { missingCmd = "unrar"; installHint = "apt install unrar / yum install unrar / brew install unrar"; } if (missingCmd) { friendlyError = `Command '${missingCmd}' not found on remote server. Please install it first: ${installHint}`; } } return res.status(500).json({ error: friendlyError }); } fileLogger.success("Archive extracted successfully", { operation: "extract_archive", sessionId, archivePath, extractPath: targetPath, }); res.json({ success: true, message: "Archive extracted successfully", extractPath: targetPath, }); }); stream.on("error", (streamErr) => { fileLogger.error("SSH extractArchive stream error:", streamErr, { operation: "extract_archive", sessionId, archivePath, }); if (!res.headersSent) { res .status(500) .json({ error: "Stream error while extracting archive" }); } }); }); }); // Route: Compress files/folders (requires JWT) // POST /ssh/file_manager/ssh/compressFiles app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { const { sessionId, paths, archiveName, format } = req.body; if ( !sessionId || !paths || !Array.isArray(paths) || paths.length === 0 || !archiveName ) { return res.status(400).json({ error: "Missing required parameters" }); } const session = sshSessions[sessionId]; if (!session || !session.isConnected) { return res.status(400).json({ error: "SSH session not connected" }); } session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const compressionFormat = format || "zip"; let compressCommand = ""; const firstPath = paths[0]; const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/"; const fileNames = paths .map((p) => { const name = p.split("/").pop(); return `"${name}"`; }) .join(" "); let archivePath = ""; if (archiveName.includes("/")) { archivePath = archiveName; } else { archivePath = workingDir.endsWith("/") ? `${workingDir}${archiveName}` : `${workingDir}/${archiveName}`; } if (compressionFormat === "zip") { compressCommand = `cd "${workingDir}" && zip -r "${archivePath}" ${fileNames}`; } else if (compressionFormat === "tar.gz" || compressionFormat === "tgz") { compressCommand = `cd "${workingDir}" && tar -czf "${archivePath}" ${fileNames}`; } else if (compressionFormat === "tar.bz2" || compressionFormat === "tbz2") { compressCommand = `cd "${workingDir}" && tar -cjf "${archivePath}" ${fileNames}`; } else if (compressionFormat === "tar.xz") { compressCommand = `cd "${workingDir}" && tar -cJf "${archivePath}" ${fileNames}`; } else if (compressionFormat === "tar") { compressCommand = `cd "${workingDir}" && tar -cf "${archivePath}" ${fileNames}`; } else if (compressionFormat === "7z") { compressCommand = `cd "${workingDir}" && 7z a "${archivePath}" ${fileNames}`; } else { return res.status(400).json({ error: "Unsupported compression format" }); } fileLogger.info("Compressing files", { operation: "compress_files", sessionId, paths, archivePath, format: compressionFormat, command: compressCommand, }); session.client.exec(compressCommand, (err, stream) => { if (err) { fileLogger.error("SSH exec error during compress:", err, { operation: "compress_files", sessionId, paths, }); return res .status(500) .json({ error: "Failed to execute compress command" }); } let errorOutput = ""; stream.on("data", (data: Buffer) => { fileLogger.debug("Compress stdout", { operation: "compress_files", sessionId, output: data.toString(), }); }); stream.stderr.on("data", (data: Buffer) => { errorOutput += data.toString(); fileLogger.debug("Compress stderr", { operation: "compress_files", sessionId, error: data.toString(), }); }); stream.on("close", (code: number) => { if (code !== 0) { fileLogger.error("Compress command failed", { operation: "compress_files", sessionId, paths, archivePath, exitCode: code, error: errorOutput, }); let friendlyError = errorOutput || "Failed to compress files"; if ( errorOutput.includes("command not found") || errorOutput.includes("not found") ) { const commandMap: Record = { zip: { cmd: "zip", install: "apt install zip / yum install zip / brew install zip", }, "tar.gz": { cmd: "tar", install: "Usually pre-installed on Linux/Unix systems", }, "tar.bz2": { cmd: "tar", install: "Usually pre-installed on Linux/Unix systems", }, "tar.xz": { cmd: "tar", install: "Usually pre-installed on Linux/Unix systems", }, tar: { cmd: "tar", install: "Usually pre-installed on Linux/Unix systems", }, "7z": { cmd: "7z", install: "apt install p7zip-full / yum install p7zip / brew install p7zip", }, }; const info = commandMap[compressionFormat]; if (info) { friendlyError = `Command '${info.cmd}' not found on remote server. Please install it first: ${info.install}`; } } return res.status(500).json({ error: friendlyError }); } fileLogger.success("Files compressed successfully", { operation: "compress_files", sessionId, paths, archivePath, format: compressionFormat, }); res.json({ success: true, message: "Files compressed successfully", archivePath: archivePath, }); }); stream.on("error", (streamErr) => { fileLogger.error("SSH compressFiles stream error:", streamErr, { operation: "compress_files", sessionId, paths, }); if (!res.headersSent) { res.status(500).json({ error: "Stream error while compressing files" }); } }); }); }); process.on("SIGINT", () => { Object.keys(sshSessions).forEach(cleanupSession); process.exit(0); }); process.on("SIGTERM", () => { Object.keys(sshSessions).forEach(cleanupSession); process.exit(0); }); const PORT = 30004; try { const server = app.listen(PORT, async () => { try { await authManager.initialize(); } catch (err) { fileLogger.error("Failed to initialize AuthManager", err, { operation: "auth_init_error", }); } }); server.on("error", (err) => { fileLogger.error("File Manager server error", err, { operation: "file_manager_server_error", port: PORT, }); }); } catch (err) { fileLogger.error("Failed to start File Manager server", err, { operation: "file_manager_server_start_failed", port: PORT, }); }