1117 lines
31 KiB
JavaScript
1117 lines
31 KiB
JavaScript
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`);
|
|
}); |