import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import crypto from 'crypto'; import fs from 'fs/promises'; import path from 'path'; dotenv.config(); const app = express(); const CONFIG_FILE = path.join(process.cwd(), 'config.json'); // Load configuration let config = {}; async function loadConfig() { try { const configData = await fs.readFile(CONFIG_FILE, 'utf8'); const newConfig = JSON.parse(configData); // Check if root directory changed const oldRootDirectory = config.server?.rootDirectory; const newRootDirectory = newConfig.server?.rootDirectory; config = newConfig; console.log('✅ API Server: Configuration loaded successfully'); // If root directory changed, refresh file system cache if (oldRootDirectory && newRootDirectory && oldRootDirectory !== newRootDirectory) { console.log(`📁 API Server: Root directory changed from ${oldRootDirectory} to ${newRootDirectory}`); console.log('🔄 API Server: Refreshing file system cache...'); await refreshFileSystemCache(); } return config; } catch (error) { console.error('❌ API Server: Error loading config:', error.message); // Create default config if file doesn't exist config = { server: { name: "File Manager API Server", port: 3001, rootDirectory: "/home/project", logLevel: "info", environment: "development", capabilities: [ "file-transfer", "server-management", "health-monitoring", "filesystem-browsing" ] }, filesystem: { maxDepth: 10, cacheSettings: { ttl: 60000, refreshInterval: 300000 }, watchEnabled: true, maxFileSize: 104857600 }, transfer: { maxConcurrentTransfers: 5, uploadTimeout: 300000, maxRequestSize: 104857600 }, security: { corsOrigins: [ "http://localhost:3000", "http://localhost:5173" ], requestTimeout: 30000, rateLimiting: { windowMs: 900000, maxRequests: 1000 } }, logging: { requests: true, errors: true, filePath: "./logs/api-server.log" }, health: { checkEnabled: true, checkInterval: 60000 } }; await saveConfig(); return config; } } async function saveConfig() { try { await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); console.log('✅ API Server: Configuration saved successfully'); return true; } catch (error) { console.error('❌ API Server: Error saving config:', error.message); return false; } } // Watch config file for changes async function watchConfigFile() { try { const { watch } = await import('fs'); console.log('👁️ API Server: Starting config file watcher...'); const watcher = watch(CONFIG_FILE, { persistent: false }, async (eventType, filename) => { if (eventType === 'change' && filename === 'config.json') { console.log('📝 API Server: Config file changed, reloading...'); // Add a small delay to ensure file write is complete setTimeout(async () => { try { await loadConfig(); console.log('✅ API Server: Configuration reloaded successfully'); } catch (error) { console.error('❌ API Server: Error reloading configuration:', error.message); } }, 100); } }); watcher.on('error', (error) => { console.error('❌ API Server: Config file watcher error:', error.message); }); console.log('✅ API Server: Config file watcher started'); return watcher; } catch (error) { console.error('❌ API Server: Failed to start config file watcher:', error.message); return null; } } // Get current configuration function getConfig() { return config; } const API_KEY = process.env.API_KEY; const PORT = process.env.PORT || config.server?.port || 3001; // Middleware app.use(cors({ origin: config.security?.corsOrigins || ["http://localhost:3000", "http://localhost:5173"], credentials: true })); app.use(express.json()); // API Key validation middleware const validateApiKey = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ success: false, error: 'Authorization header missing' }); } const token = authHeader.replace('Bearer ', ''); if (token !== API_KEY) { return res.status(401).json({ success: false, error: 'Invalid API key' }); } next(); }; // Request logging middleware app.use((req, res, next) => { if (config.logging?.requests) { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] API Server: ${req.method} ${req.path} - ${req.ip}`); } next(); }); // File system cache let fileSystemCache = { data: null, lastUpdate: null, ttl: config.filesystem?.cacheSettings?.ttl || 60000 }; // Helper function to resolve file path correctly function resolveFilePath(relativePath) { const rootDir = config.server.rootDirectory; // If relativePath is already absolute and starts with rootDir, use it as is if (path.isAbsolute(relativePath) && relativePath.startsWith(rootDir)) { return relativePath; } // If relativePath is absolute but doesn't start with rootDir, // treat it as relative to rootDir if (path.isAbsolute(relativePath)) { // Remove leading slash and join with rootDir const cleanPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath; return path.join(rootDir, cleanPath); } // If relativePath is relative, join with rootDir return path.join(rootDir, relativePath); } // Helper function to get relative path from absolute path function getRelativePath(absolutePath) { const rootDir = config.server.rootDirectory; // If path starts with rootDir, remove it to get relative path if (absolutePath.startsWith(rootDir)) { const relativePath = absolutePath.slice(rootDir.length); return relativePath.startsWith('/') ? relativePath : '/' + relativePath; } // If path doesn't start with rootDir, return as is return absolutePath; } // Dangerous directories to skip when scanning from root const DANGEROUS_DIRECTORIES = new Set([ '/proc', '/sys', '/dev', '/run', '/tmp', '/var/run', '/var/lock', '/var/tmp', '/boot', '/lost+found' ]); // Helper function to check if directory should be skipped function shouldSkipDirectory(dirPath) { // Skip dangerous system directories if (DANGEROUS_DIRECTORIES.has(dirPath)) { return true; } // Skip any subdirectories of dangerous directories for (const dangerousDir of DANGEROUS_DIRECTORIES) { if (dirPath.startsWith(dangerousDir + '/')) { return true; } } // Skip hidden directories when scanning from root const rootDir = config.server.rootDirectory; if (rootDir === '/' && path.basename(dirPath).startsWith('.')) { return true; } return false; } // Helper function to get file stats safely async function getFileStats(filePath) { try { const stats = await fs.stat(filePath); return { isDirectory: stats.isDirectory(), isFile: stats.isFile(), size: stats.size, lastModified: stats.mtime, created: stats.birthtime, permissions: stats.mode }; } catch (error) { return null; } } // Helper function to scan directory recursively with safety limits async function scanDirectory(dirPath, currentDepth = 0, maxDepth = 10) { if (currentDepth >= maxDepth) { console.log(`⚠️ API Server: Max depth ${maxDepth} reached for ${dirPath}`); return []; } // Check if directory should be skipped for safety if (shouldSkipDirectory(dirPath)) { console.log(`⚠️ API Server: Skipping dangerous directory: ${dirPath}`); return []; } try { const entries = await fs.readdir(dirPath); const files = []; let processedCount = 0; const maxEntriesPerDirectory = 1000; // Limit entries per directory for (const entry of entries) { // Limit number of entries processed per directory if (processedCount >= maxEntriesPerDirectory) { console.log(`⚠️ API Server: Directory ${dirPath} has too many entries, limiting to ${maxEntriesPerDirectory}`); break; } const fullPath = path.join(dirPath, entry); // Skip dangerous directories if (shouldSkipDirectory(fullPath)) { continue; } const stats = await getFileStats(fullPath); if (!stats) continue; const relativePath = getRelativePath(fullPath); const fileItem = { id: crypto.randomUUID(), name: entry, path: relativePath, type: stats.isDirectory ? 'folder' : 'file', size: stats.isFile ? stats.size : undefined, lastModified: stats.lastModified, created: stats.created, permissions: stats.permissions }; if (stats.isDirectory && currentDepth < maxDepth - 1) { try { fileItem.children = await scanDirectory(fullPath, currentDepth + 1, maxDepth); } catch (error) { console.warn(`⚠️ API Server: Cannot read directory ${fullPath}: ${error.message}`); fileItem.children = []; } } files.push(fileItem); processedCount++; } return files.sort((a, b) => { // Folders first, then files, both alphabetically if (a.type !== b.type) { return a.type === 'folder' ? -1 : 1; } return a.name.localeCompare(b.name); }); } catch (error) { console.error(`❌ API Server: Error scanning directory ${dirPath}: ${error.message}`); return []; } } // Helper function to refresh file system cache with safety limits async function refreshFileSystemCache() { try { const rootDir = config.server.rootDirectory; console.log(`🔍 API Server: Scanning file system from root: ${rootDir}`); // Special handling for root directory if (rootDir === '/') { console.log(`⚠️ API Server: Root directory is '/', applying safety limits`); } const startTime = Date.now(); // Use reduced max depth for root directory scanning const maxDepth = rootDir === '/' ? 3 : (config.filesystem?.maxDepth || 10); console.log(`📊 API Server: Using max depth: ${maxDepth}`); const files = await scanDirectory(rootDir, 0, maxDepth); fileSystemCache = { data: files, lastUpdate: new Date().toISOString(), ttl: config.filesystem?.cacheSettings?.ttl || 60000, scanTime: Date.now() - startTime, totalFiles: countFiles(files), rootDirectory: rootDir, maxDepth: maxDepth }; console.log(`✅ API Server: File system scan completed in ${fileSystemCache.scanTime}ms, found ${fileSystemCache.totalFiles} items`); return true; } catch (error) { console.error('❌ API Server: Error refreshing file system cache:', error); return false; } } // Helper function to count files recursively function countFiles(files) { let count = 0; for (const file of files) { count++; if (file.children) { count += countFiles(file.children); } } return count; } // Check if cache is valid function isCacheValid() { if (!fileSystemCache.data || !fileSystemCache.lastUpdate) { return false; } const now = Date.now(); const lastUpdate = new Date(fileSystemCache.lastUpdate).getTime(); return (now - lastUpdate) < fileSystemCache.ttl; } // Mock data for servers (keeping existing functionality) const mockServers = [ { id: 'server1', name: 'Production Server', address: '192.168.1.100', port: 22, type: 'sftp', status: 'online', lastSeen: new Date().toISOString() }, { id: 'server2', name: 'Development Server', address: '192.168.1.101', port: 22, type: 'sftp', status: 'offline', lastSeen: new Date(Date.now() - 3600000).toISOString() } ]; // Routes // Get configuration app.get('/api/v1/config', validateApiKey, (req, res) => { res.json({ success: true, data: config }); }); // Update configuration app.post('/api/v1/config', validateApiKey, async (req, res) => { try { const updates = req.body; // Deep merge configuration function deepMerge(target, source) { for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key]) target[key] = {}; deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } } const oldRootDirectory = config.server?.rootDirectory; deepMerge(config, updates); const saved = await saveConfig(); if (saved) { // Refresh file system if root directory changed const newRootDirectory = config.server?.rootDirectory; if (oldRootDirectory !== newRootDirectory) { console.log(`📁 API Server: Root directory updated from ${oldRootDirectory} to ${newRootDirectory}`); await refreshFileSystemCache(); } res.json({ success: true, message: 'Configuration updated successfully', data: config }); } else { res.status(500).json({ success: false, error: 'Failed to save configuration' }); } } catch (error) { res.status(500).json({ success: false, error: 'Failed to update configuration', details: error.message }); } }); // Update root directory app.post('/api/v1/config/root-directory', validateApiKey, async (req, res) => { try { const { rootDirectory } = req.body; console.log(`📡 API Server: Received root directory update request`); console.log(`📡 API Server: Request body:`, req.body); console.log(`📡 API Server: Headers:`, req.headers); if (!rootDirectory) { console.log(`❌ API Server: Root directory is missing from request`); return res.status(400).json({ success: false, error: 'Root directory is required' }); } const oldRootDirectory = config.server.rootDirectory; console.log(`📁 API Server: Updating root directory from "${oldRootDirectory}" to "${rootDirectory}"`); // Validate root directory if (rootDirectory === '/') { console.log(`⚠️ API Server: Warning - Setting root directory to '/' will apply safety limits`); } // Update the configuration config.server.rootDirectory = rootDirectory; // Save the configuration to file console.log(`💾 API Server: Saving configuration to ${CONFIG_FILE}`); const saved = await saveConfig(); if (saved) { console.log(`✅ API Server: Root directory updated successfully to "${rootDirectory}"`); console.log(`🔄 API Server: Refreshing file system cache...`); // Refresh file system cache with new root directory const refreshed = await refreshFileSystemCache(); console.log(`✅ API Server: File system cache refresh ${refreshed ? 'successful' : 'failed'}`); res.json({ success: true, message: 'Root directory updated successfully', data: { rootDirectory, previousDirectory: oldRootDirectory, configSaved: true, cacheRefreshed: refreshed, safetyLimitsApplied: rootDirectory === '/' } }); } else { console.error(`❌ API Server: Failed to save configuration`); res.status(500).json({ success: false, error: 'Failed to save configuration' }); } } catch (error) { console.error(`❌ API Server: Error updating root directory:`, error); res.status(500).json({ success: false, error: 'Failed to update root directory', details: error.message }); } }); // Health check endpoint app.get('/api/v1/health', validateApiKey, (req, res) => { const uptime = process.uptime(); const timestamp = new Date().toISOString(); res.json({ success: true, data: { status: 'healthy', timestamp, uptime: Math.floor(uptime), version: '1.0.0', environment: config.server?.environment || 'development', rootDirectory: config.server.rootDirectory, fileSystemCache: { lastUpdate: fileSystemCache.lastUpdate, isValid: isCacheValid(), totalFiles: fileSystemCache.totalFiles || 0, maxDepth: fileSystemCache.maxDepth, safetyLimitsActive: config.server.rootDirectory === '/' } } }); }); // Get server information app.get('/api/v1/server-info', validateApiKey, (req, res) => { res.json({ success: true, data: { name: config.server.name, version: '1.0.0', rootDirectory: config.server.rootDirectory, capabilities: config.server.capabilities, limits: { maxFileSize: `${Math.round(config.filesystem.maxFileSize / 1024 / 1024)}MB`, maxConcurrentTransfers: config.transfer.maxConcurrentTransfers, maxScanDepth: config.filesystem.maxDepth, safetyLimitsActive: config.server.rootDirectory === '/' } } }); }); // Get file system structure app.get('/api/v1/filesystem', validateApiKey, async (req, res) => { try { // Check if cache is valid, refresh if needed if (!isCacheValid()) { const refreshed = await refreshFileSystemCache(); if (!refreshed) { return res.status(500).json({ success: false, error: 'Failed to scan file system' }); } } res.json({ success: true, data: { files: fileSystemCache.data || [], rootDirectory: config.server.rootDirectory, lastUpdate: fileSystemCache.lastUpdate, scanTime: fileSystemCache.scanTime, totalFiles: fileSystemCache.totalFiles, maxDepth: fileSystemCache.maxDepth, safetyLimitsActive: config.server.rootDirectory === '/', cached: true } }); } catch (error) { console.error('❌ API Server: Error getting file system:', error); res.status(500).json({ success: false, error: 'Failed to get file system structure', details: error.message }); } }); // Refresh file system cache manually app.post('/api/v1/filesystem/refresh', validateApiKey, async (req, res) => { try { const refreshed = await refreshFileSystemCache(); if (refreshed) { res.json({ success: true, data: { message: 'File system cache refreshed successfully', lastUpdate: fileSystemCache.lastUpdate, scanTime: fileSystemCache.scanTime, totalFiles: fileSystemCache.totalFiles, maxDepth: fileSystemCache.maxDepth, safetyLimitsActive: config.server.rootDirectory === '/' } }); } else { res.status(500).json({ success: false, error: 'Failed to refresh file system cache' }); } } catch (error) { console.error('❌ API Server: Error refreshing file system:', error); res.status(500).json({ success: false, error: 'Failed to refresh file system cache', details: error.message }); } }); // Get specific directory contents app.get('/api/v1/filesystem/directory', validateApiKey, async (req, res) => { const { path: dirPath = '/' } = req.query; try { const fullPath = resolveFilePath(dirPath); console.log(`📁 API Server: Getting directory contents for: ${dirPath} -> ${fullPath}`); const files = await scanDirectory(fullPath, 0, 2); // Limit depth for directory listing res.json({ success: true, data: { path: dirPath, files, timestamp: new Date().toISOString() } }); } catch (error) { console.error('❌ API Server: Error getting directory contents:', error); res.status(500).json({ success: false, error: 'Failed to get directory contents', details: error.message }); } }); // Read file content app.get('/api/v1/filesystem/file-content', validateApiKey, async (req, res) => { try { const { path: filePath } = req.query; if (!filePath) { return res.status(400).json({ success: false, error: 'File path is required' }); } const fullPath = resolveFilePath(filePath); console.log(`📄 API Server: Reading file content from: ${filePath} -> ${fullPath}`); // Check if file exists and is a file const stats = await getFileStats(fullPath); if (!stats) { return res.status(404).json({ success: false, error: 'File not found' }); } if (!stats.isFile) { return res.status(400).json({ success: false, error: 'Path is not a file' }); } // Check file size limit (100MB default) const maxFileSize = config.filesystem?.maxFileSize || 104857600; if (stats.size > maxFileSize) { return res.status(413).json({ success: false, error: `File too large. Maximum size: ${Math.round(maxFileSize / 1024 / 1024)}MB` }); } const content = await fs.readFile(fullPath, 'utf8'); console.log(`✅ API Server: File content read successfully, size: ${content.length} characters`); res.json({ success: true, data: { content, path: filePath, size: stats.size, lastModified: stats.lastModified, encoding: 'utf8' } }); } catch (error) { console.error('❌ API Server: Error reading file content:', error); if (error.code === 'ENOENT') { res.status(404).json({ success: false, error: 'File not found' }); } else if (error.code === 'EACCES') { res.status(403).json({ success: false, error: 'Permission denied' }); } else if (error.code === 'EISDIR') { res.status(400).json({ success: false, error: 'Path is a directory, not a file' }); } else { res.status(500).json({ success: false, error: 'Failed to read file content', details: error.message }); } } }); // Write file content app.post('/api/v1/filesystem/file-content', validateApiKey, async (req, res) => { try { const { path: filePath, content } = req.body; if (!filePath) { return res.status(400).json({ success: false, error: 'File path is required' }); } if (content === undefined || content === null) { return res.status(400).json({ success: false, error: 'File content is required' }); } const fullPath = resolveFilePath(filePath); console.log(`📄 API Server: Writing file content to: ${filePath} -> ${fullPath}`); console.log(`📄 API Server: Content length: ${content.length} characters`); // Ensure parent directory exists const parentDir = path.dirname(fullPath); await fs.mkdir(parentDir, { recursive: true }); // Write file content await fs.writeFile(fullPath, content, 'utf8'); // Get file stats after writing const stats = await getFileStats(fullPath); // Refresh file system cache await refreshFileSystemCache(); console.log(`✅ API Server: File content written successfully`); res.json({ success: true, message: 'File content written successfully', data: { path: filePath, size: stats?.size || content.length, lastModified: stats?.lastModified || new Date(), encoding: 'utf8' } }); } catch (error) { console.error('❌ API Server: Error writing file content:', error); if (error.code === 'EACCES') { res.status(403).json({ success: false, error: 'Permission denied' }); } else if (error.code === 'ENOSPC') { res.status(507).json({ success: false, error: 'Insufficient storage space' }); } else { res.status(500).json({ success: false, error: 'Failed to write file content', details: error.message }); } } }); // Create folder app.post('/api/v1/filesystem/folder', validateApiKey, async (req, res) => { try { const { parentPath, name } = req.body; if (!parentPath || !name) { return res.status(400).json({ success: false, error: 'Parent path and name are required' }); } const fullParentPath = resolveFilePath(parentPath); const newFolderPath = path.join(fullParentPath, name); console.log(`📁 API Server: Creating folder: ${parentPath}/${name} -> ${newFolderPath}`); await fs.mkdir(newFolderPath, { recursive: true }); await refreshFileSystemCache(); console.log(`✅ API Server: Folder created successfully: ${newFolderPath}`); res.json({ success: true, message: 'Folder created successfully', data: { path: newFolderPath } }); } catch (error) { console.error('❌ API Server: Error creating folder:', error); res.status(500).json({ success: false, error: 'Failed to create folder', details: error.message }); } }); // Create file app.post('/api/v1/filesystem/file', validateApiKey, async (req, res) => { try { const { parentPath, name } = req.body; if (!parentPath || !name) { return res.status(400).json({ success: false, error: 'Parent path and name are required' }); } const fullParentPath = resolveFilePath(parentPath); const newFilePath = path.join(fullParentPath, name); console.log(`📄 API Server: Creating file: ${parentPath}/${name} -> ${newFilePath}`); await fs.writeFile(newFilePath, '', 'utf8'); await refreshFileSystemCache(); console.log(`✅ API Server: File created successfully: ${newFilePath}`); res.json({ success: true, message: 'File created successfully', data: { path: newFilePath } }); } catch (error) { console.error('❌ API Server: Error creating file:', error); res.status(500).json({ success: false, error: 'Failed to create file', details: error.message }); } }); // Delete item - Updated to read path from query parameter app.delete('/api/v1/filesystem/item', validateApiKey, async (req, res) => { try { const itemPath = req.query.path; if (!itemPath) { return res.status(400).json({ success: false, error: 'Item path is required' }); } const fullPath = resolveFilePath(itemPath); console.log(`🗑️ API Server: Attempting to delete item: ${itemPath} -> ${fullPath}`); const stats = await getFileStats(fullPath); if (!stats) { console.log(`❌ API Server: Item not found: ${fullPath}`); return res.status(404).json({ success: false, error: 'Item not found' }); } if (stats.isDirectory) { console.log(`📁 API Server: Deleting directory: ${fullPath}`); await fs.rm(fullPath, { recursive: true, force: true }); } else { console.log(`📄 API Server: Deleting file: ${fullPath}`); await fs.unlink(fullPath); } await refreshFileSystemCache(); console.log(`✅ API Server: Item deleted successfully: ${fullPath}`); res.json({ success: true, message: 'Item deleted successfully' }); } catch (error) { console.error('❌ API Server: Error deleting item:', error); res.status(500).json({ success: false, error: 'Failed to delete item', details: error.message }); } }); // Get servers list (existing functionality) app.get('/api/v1/servers', validateApiKey, (req, res) => { res.json({ success: true, data: { servers: mockServers, total: mockServers.length } }); }); // Get specific server (existing functionality) app.get('/api/v1/servers/:serverId', validateApiKey, (req, res) => { const { serverId } = req.params; const server = mockServers.find(s => s.id === serverId); if (!server) { return res.status(404).json({ success: false, error: 'Server not found' }); } res.json({ success: true, data: server }); }); // Test server connection (existing functionality) app.post('/api/v1/servers/:serverId/test', validateApiKey, (req, res) => { const { serverId } = req.params; const server = mockServers.find(s => s.id === serverId); if (!server) { return res.status(404).json({ success: false, error: 'Server not found' }); } const isOnline = Math.random() > 0.3; setTimeout(() => { res.json({ success: true, data: { serverId, connected: isOnline, responseTime: Math.floor(Math.random() * 200) + 50, timestamp: new Date().toISOString() } }); }, 1000 + Math.random() * 2000); }); // Error handling middleware app.use((error, req, res, next) => { if (config.logging?.errors) { console.error('❌ API Server Error:', error); } res.status(500).json({ success: false, error: 'Internal server error', details: process.env.NODE_ENV === 'development' ? error.message : undefined }); }); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found' }); }); // Initialize file system cache on startup async function initializeServer() { console.log(`🔧 API Server starting...`); // Load configuration first await loadConfig(); console.log(`📁 API Server: Root directory: ${config.server.rootDirectory}`); console.log(`🔍 API Server: Max scan depth: ${config.filesystem.maxDepth}`); if (config.server.rootDirectory === '/') { console.log(`⚠️ API Server: WARNING - Root directory is '/', safety limits will be applied`); } // Start config file watcher await watchConfigFile(); // Initial file system scan await refreshFileSystemCache(); // Set up periodic cache refresh const refreshInterval = config.filesystem?.cacheSettings?.refreshInterval || 300000; // 5 minutes default setInterval(async () => { console.log('🔄 API Server: Performing scheduled file system cache refresh...'); await refreshFileSystemCache(); }, refreshInterval); } // Start server app.listen(PORT, async () => { await initializeServer(); console.log(`🔧 API Server running on port ${PORT}`); console.log(`🔑 API Server: API Key: ${API_KEY ? 'Configured' : 'Missing'}`); console.log(`🌍 API Server: Environment: ${config.server?.environment || 'development'}`); console.log(`👁️ API Server: Config file watching: Enabled`); console.log(`📋 API Server: Available endpoints:`); console.log(` GET /api/v1/config`); console.log(` POST /api/v1/config`); console.log(` POST /api/v1/config/root-directory`); console.log(` GET /api/v1/health`); console.log(` GET /api/v1/server-info`); console.log(` GET /api/v1/filesystem`); console.log(` POST /api/v1/filesystem/refresh`); console.log(` GET /api/v1/filesystem/directory`); console.log(` GET /api/v1/filesystem/file-content`); console.log(` POST /api/v1/filesystem/file-content`); console.log(` POST /api/v1/filesystem/folder`); console.log(` POST /api/v1/filesystem/file`); console.log(` DELETE /api/v1/filesystem/item`); console.log(` GET /api/v1/servers`); console.log(` GET /api/v1/servers/:id`); console.log(` POST /api/v1/servers/:id/test`); });