/** * Files API Endpoint * * GET /api/files/:host/:container - Get volume mounts for a container */ import { Router } from 'express'; import { Client } from 'ssh2'; import { readFileSync } from 'fs'; import { homedir } from 'os'; import { getHostConfigs } from '../config'; const router = Router(); interface SSHConnectionConfig { host: string; port?: number; username: string; privateKey?: Buffer; } interface VolumeMount { source: string; destination: string; mode: string; } interface FilesResponse { volumes: VolumeMount[]; error?: string; } async function connectSSH(config: SSHConnectionConfig, timeout: number = 30000): Promise { return new Promise((resolve, reject) => { const conn = new Client(); const timeoutId = setTimeout(() => { conn.end(); reject(new Error('Connection timeout')); }, timeout); conn.on('ready', () => { clearTimeout(timeoutId); resolve(conn); }); conn.on('error', (err) => { clearTimeout(timeoutId); reject(err); }); conn.connect({ host: config.host, port: config.port || 22, username: config.username, privateKey: config.privateKey, readyTimeout: timeout, }); }); } async function execSSH(conn: Client, command: string): Promise { return new Promise((resolve, reject) => { conn.exec(command, (err, stream) => { if (err) { reject(err); return; } let output = ''; stream.on('close', () => resolve(output)); stream.on('data', (data: Buffer) => { output += data.toString(); }); stream.stderr.on('data', (data: Buffer) => { output += data.toString(); }); }); }); } async function getContainerVolumes( ip: string, containerName: string, sshUser: string, sshKeyPath?: string, sshPort?: number ): Promise { try { // Load SSH key const keyPath = sshKeyPath || `${homedir()}/.ssh/id_ed25519`; let privateKey: Buffer | undefined; try { privateKey = readFileSync(keyPath); } catch { // If key file doesn't exist, try without it } const sshConfig: SSHConnectionConfig = { host: ip, port: sshPort || 22, username: sshUser, privateKey, }; const conn = await connectSSH(sshConfig, 30000); // Run docker inspect to get mounts const command = `docker inspect ${containerName} --format '{{json .Mounts}}'`; const output = await execSSH(conn, command); conn.end(); // Parse JSON output let mounts: any[] = []; try { mounts = JSON.parse(output.trim()) || []; } catch { // If parsing fails, return empty array return { volumes: [] }; } // Transform to VolumeMount format const volumes: VolumeMount[] = mounts.map((mount: any) => ({ source: mount.Source || mount.Source || '', destination: mount.Destination || mount.Destination || '', mode: mount.Mode || mount.Mode || 'rw', })).filter((v: VolumeMount) => v.source && v.destination); return { volumes }; } catch (error: any) { return { volumes: [], error: error.message || 'Failed to get container volumes', }; } } // GET /api/files/:host/:container - Get volume mounts for a container router.get('/files/:host/:container', async (req, res) => { try { const { host, container } = req.params; const hosts = getHostConfigs(); const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase()); if (!hostConfig) { const response: FilesResponse = { volumes: [], error: `Host '${host}' not found in configuration`, }; return res.status(404).json(response); } const result = await getContainerVolumes( hostConfig.ip, container, hostConfig.sshUser, hostConfig.sshKeyPath, hostConfig.sshPort ); res.json(result); } catch (error: unknown) { const errMsg = error instanceof Error ? error.message : 'Failed to get container volumes'; const response: FilesResponse = { volumes: [], error: errMsg, }; res.status(500).json(response); } }); interface FileEntry { name: string; path: string; type: 'file' | 'directory' | 'symlink'; size: number; modified: string; } interface BrowseResponse { path: string; files: FileEntry[]; error?: string; } async function browseDirectory( ip: string, path: string, sshUser: string, sshKeyPath?: string, sshPort?: number ): Promise { try { const keyPath = sshKeyPath || `${homedir()}/.ssh/id_ed25519`; let privateKey: Buffer | undefined; try { privateKey = require('fs').readFileSync(keyPath); } catch { // ignore } const sshConfig: SSHConnectionConfig = { host: ip, port: sshPort || 22, username: sshUser, privateKey, }; const conn = await connectSSH(sshConfig, 30000); const command = `ls -la --time-style=long-iso "${path}" 2>/dev/null | tail -n +2`; const output = await execSSH(conn, command); conn.end(); const files: FileEntry[] = output.trim().split('\n').filter(Boolean).map(line => { const parts = line.split(/\s+/); const perms = parts[0]; const size = parseInt(parts[4], 10) || 0; const modified = parts[5] + ' ' + parts[6]; const name = parts.slice(8).join(' '); const type = perms.startsWith('d') ? 'directory' : perms.startsWith('l') ? 'symlink' : 'file'; return { name, path: path === '/' ? `/${name}` : `${path}/${name}`, type, size, modified, }; }); return { path, files }; } catch (error: unknown) { const errMsg = error instanceof Error ? error.message : 'Failed to browse directory'; return { path, files: [], error: errMsg }; } } router.get('/files/browse/:host', async (req, res) => { try { const { host } = req.params; const path = (req.query.path as string) || '/'; const hosts = getHostConfigs(); const hostConfig = hosts.find(h => h.name.toLowerCase() === host.toLowerCase()); if (!hostConfig) { return res.status(404).json({ path, files: [], error: `Host '${host}' not found` }); } const result = await browseDirectory( hostConfig.ip, path, hostConfig.sshUser, hostConfig.sshKeyPath, hostConfig.sshPort ); res.json(result); } catch (error: unknown) { const errMsg = error instanceof Error ? error.message : 'Failed to browse directory'; res.status(500).json({ path: req.query.path as string || '/', files: [], error: errMsg }); } }); export default router;