import fs from 'fs'; import path from 'path'; import archiver, { ArchiverError } from 'archiver'; import multer, { Multer } from 'multer'; import extract from 'extract-zip'; import yauzl, { Options as YauzlOptions, ZipFile as YauzlZipFile, Entry as YauzlEntry } from 'yauzl'; import { Request, Response, Application, RequestHandler } from 'express'; const configJsonPath: string = path.resolve(__dirname, '../config.json'); const UPLOAD_TEMP_DIR: string = path.resolve(__dirname, '../upload_temp'); let currentProjectPathFromConfig: string = ''; let currentBaseDir: string = ''; async function pathExists(p: string): Promise { try { await fs.promises.access(p, fs.constants.F_OK); return true; } catch { return false; } } async function ensureDirExists(dirPath: string): Promise { if (dirPath && !(await pathExists(dirPath))) { await fs.promises.mkdir(dirPath, { recursive: true }); } } ensureDirExists(UPLOAD_TEMP_DIR).catch(err => { console.error("FATAL: Failed to create UPLOAD_TEMP_DIR on startup:", err); }); interface AppConfig { project?: string; [key: string]: any; } function updatePathsFromConfig(): boolean { const oldRawProjectPath = currentProjectPathFromConfig; const oldBaseDir = currentBaseDir; const wasPreviouslyValid = oldBaseDir !== ''; let newRawProjectPathCandidate: string = ''; let newResolvedBaseDirCandidate: string = ''; let isNowValidConfig: boolean = false; try { if (!fs.existsSync(configJsonPath)) { newRawProjectPathCandidate = ''; newResolvedBaseDirCandidate = ''; isNowValidConfig = false; } else { const rawConfigData: string = fs.readFileSync(configJsonPath, 'utf-8'); if (rawConfigData.trim() === '') { newRawProjectPathCandidate = ''; newResolvedBaseDirCandidate = ''; isNowValidConfig = false; } else { const parsedConfig: AppConfig = JSON.parse(rawConfigData); if (parsedConfig && typeof parsedConfig.project === 'string') { newRawProjectPathCandidate = parsedConfig.project; const trimmedProjectPath: string = parsedConfig.project.trim(); if (trimmedProjectPath !== '') { const configJsonDir: string = path.dirname(configJsonPath); newResolvedBaseDirCandidate = path.resolve(configJsonDir, trimmedProjectPath); isNowValidConfig = true; } else { newResolvedBaseDirCandidate = ''; isNowValidConfig = false; } } else { newRawProjectPathCandidate = ''; newResolvedBaseDirCandidate = ''; isNowValidConfig = false; } } } } catch (error: any) { const errMsg = error instanceof Error ? error.message : String(error); console.error(`[ConfigMonitor] ERROR reading/parsing ${configJsonPath}: ${errMsg}`); newRawProjectPathCandidate = ''; newResolvedBaseDirCandidate = ''; isNowValidConfig = false; } currentProjectPathFromConfig = newRawProjectPathCandidate; currentBaseDir = isNowValidConfig ? newResolvedBaseDirCandidate : ''; if (isNowValidConfig) { if (!wasPreviouslyValid) { console.log(`[ConfigMonitor] PROJECT LOADED: Project path "${currentProjectPathFromConfig.trim()}" configured. Base directory: ${currentBaseDir}`); } else if (currentBaseDir !== oldBaseDir) { console.log(`[ConfigMonitor] PROJECT CHANGED: Project path updated to "${currentProjectPathFromConfig.trim()}". New base directory: ${currentBaseDir}`); } } else { if (wasPreviouslyValid) { console.warn(`[ConfigMonitor] PROJECT UNCONFIGURED: Project path "${oldRawProjectPath}" (was base: "${oldBaseDir}") is now invalid, removed, or config error. Waiting for configuration.`); } } return isNowValidConfig; } function startConfigMonitor(): void { console.log(`[ConfigMonitor] Initializing: Monitoring ${configJsonPath} for project configuration.`); const initiallyValid = updatePathsFromConfig(); if (!initiallyValid) { console.warn(`[ConfigMonitor] Initial Status: No valid project path found in ${configJsonPath}. Waiting for configuration.`); } setInterval(() => { updatePathsFromConfig(); }, 1000); } startConfigMonitor(); const upload: Multer = multer({ dest: UPLOAD_TEMP_DIR }); function runMulterMiddleware(req: Request, res: Response, multerMiddleware: RequestHandler): Promise { return new Promise((resolve, reject) => { multerMiddleware(req, res, (err: any) => { if (err) { return reject(err); } resolve(); }); }); } async function safePathResolve(relativePathInput: string | undefined | null, checkExists: boolean = false): Promise { if (!currentBaseDir) { console.error("[safePathResolve] Error: Attempted to operate while project base directory is not configured."); throw new Error('Project base directory is not configured. Please check config.json and ensure the "project" key is set to a valid path.'); } const activeBaseDir: string = path.resolve(currentBaseDir); if (relativePathInput === '') { await ensureDirExists(activeBaseDir); if (checkExists && !(await pathExists(activeBaseDir))) { throw new Error(`Base project directory not found: ${activeBaseDir}`); } return activeBaseDir; } if (typeof relativePathInput !== 'string' || relativePathInput.trim() === '') { throw new Error('Invalid path provided: path is empty or not a string (and not an intentional empty string for base directory).'); } const trimmedRelativePath: string = relativePathInput.trim(); const resolvedPath: string = path.resolve(activeBaseDir, trimmedRelativePath); if (!resolvedPath.startsWith(activeBaseDir + path.sep) && resolvedPath !== activeBaseDir) { console.error(`[safePathResolve] Unsafe path attempt: Resolved "${resolvedPath}" is outside base "${activeBaseDir}" from relative "${trimmedRelativePath}"`); throw new Error('Unsafe path: Access denied due to path traversal attempt.'); } if (checkExists && !(await pathExists(resolvedPath))) { const displayPath: string = path.relative(activeBaseDir, resolvedPath) || trimmedRelativePath; console.warn(`[safePathResolve] Path not found: "${displayPath}" (resolved from "${trimmedRelativePath}" within base "${activeBaseDir}")`); throw new Error(`Path not found: ${displayPath}`); } return resolvedPath; } interface FileManagerRouteParams { req: Request; res: Response; app?: Application; } type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all"; interface FileManagerExpressRouteModule { method: HttpMethod; path: string; install: (params: FileManagerRouteParams) => Promise | void; } export const modules: FileManagerExpressRouteModule[] = [ { method: 'post', path: '/file/list', // <--- Here it is install: async ({ req, res }: FileManagerRouteParams) => { try { const dirToList: string = await safePathResolve(''); const itemNames: string[] = await fs.promises.readdir(dirToList); const items = await Promise.all(itemNames.map(async (name) => { const fullPath: string = path.join(dirToList, name); const stat: fs.Stats = await fs.promises.stat(fullPath); return { name, isDir: stat.isDirectory() }; })); res.json(items); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/list:', errMsg, e); if (errMsg.includes('Project base directory is not configured')) { res.status(503).send(errMsg); } else if (e instanceof Error && e.message.startsWith('Path not found')) { res.status(404).send("Root project directory not found or accessible."); } else { res.status(500).send(errMsg); } } }, }, { method: 'post', path: '/file/list', install: async ({ req, res }: FileManagerRouteParams) => { try { const dirToList: string = await safePathResolve(''); const itemNames: string[] = await fs.promises.readdir(dirToList); const items = await Promise.all(itemNames.map(async (name) => { const fullPath: string = path.join(dirToList, name); const stat: fs.Stats = await fs.promises.stat(fullPath); return { name, isDir: stat.isDirectory() }; })); res.json(items); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/list:', errMsg, e); if (errMsg.includes('Project base directory is not configured')) { res.status(503).send(errMsg); } else if (e instanceof Error && e.message.startsWith('Path not found')) { res.status(404).send("Root project directory not found or accessible."); } else { res.status(500).send(errMsg); } } }, }, { method: 'post', path: '/file/open', install: async ({ req, res }: FileManagerRouteParams) => { try { const filePath: string = await safePathResolve(req.body.file, true); const content: string = await fs.promises.readFile(filePath, 'utf8'); res.send(content); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/open:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/save', install: async ({ req, res }: FileManagerRouteParams) => { try { const fileRelativePath: string = req.body.file; const content: string = req.body.content; if (typeof content !== 'string') { res.status(400).send('Invalid content for file save.'); return; } const filePath: string = await safePathResolve(fileRelativePath, false); const dirForFile: string = path.dirname(filePath); await ensureDirExists(dirForFile); await fs.promises.writeFile(filePath, content, 'utf8'); res.send('File saved'); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/save:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/upload', install: async ({ req, res }: FileManagerRouteParams) => { try { if (!currentBaseDir) { throw new Error('Project base directory is not configured. Cannot accept uploads.'); } await runMulterMiddleware(req, res, upload.single('file')); if (!req.file) { res.status(400).send('No file uploaded.'); return; } const uploadedFile: Express.Multer.File = req.file as Express.Multer.File; const relativeTargetSubfolder: string = typeof req.body.path === 'string' && req.body.path.trim() !== '' ? req.body.path.trim() : ''; const targetDirectory: string = await safePathResolve(relativeTargetSubfolder, false); try { const targetStat: fs.Stats = await fs.promises.stat(targetDirectory); if (!targetStat.isDirectory()) { await fs.promises.unlink(uploadedFile.path); res.status(400).send('Target path exists but is not a directory.'); return; } } catch (statError: any) { if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') { await ensureDirExists(targetDirectory); } else { await fs.promises.unlink(uploadedFile.path); throw statError; } } const finalDestination: string = path.join(targetDirectory, uploadedFile.originalname); if (!finalDestination.startsWith(targetDirectory + path.sep) && finalDestination !== targetDirectory) { await fs.promises.unlink(uploadedFile.path); throw new Error("Upload failed: constructed final path is outside target directory."); } await fs.promises.rename(uploadedFile.path, finalDestination); const displayPath: string = path.relative(path.resolve(currentBaseDir), finalDestination).split(path.sep).join('/') || uploadedFile.originalname; res.send('File uploaded to ' + displayPath); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/upload handler:', errMsg, e); const currentReqFile = req.file as Express.Multer.File | undefined; if (currentReqFile && currentReqFile.path && await pathExists(currentReqFile.path)) { await fs.promises.unlink(currentReqFile.path).catch(unlinkErr => console.error("Failed to cleanup temp upload file:", (unlinkErr as Error).message)); } if (!res.headersSent) { let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof multer.MulterError) statusCode = 400; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; res.status(statusCode).send(errMsg); } } }, }, { method: 'post', path: '/file/download', install: async ({ req, res }: FileManagerRouteParams) => { try { const filePath: string = await safePathResolve(req.body.file, true); const stat: fs.Stats = await fs.promises.stat(filePath); if (!stat.isFile()) { res.status(400).send('Specified path is not a file.'); return; } res.download(filePath); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/download:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/download-zip', install: async ({ req, res }: FileManagerRouteParams) => { try { const folderRelativePath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : ''; const folderToZip: string = await safePathResolve(folderRelativePath, true); const stat: fs.Stats = await fs.promises.stat(folderToZip); if (!stat.isDirectory()) { res.status(400).send('Specified path is not a directory.'); return; } const zipName: string = (path.basename(folderToZip) || 'archive') + '.zip'; res.setHeader('Content-Disposition', `attachment; filename="${zipName}"`); res.setHeader('Content-Type', 'application/zip'); const archive = archiver('zip', { zlib: { level: 9 } }); archive.on('warning', (err: ArchiverError) => { console.warn('[Archiver Warning]', err.code, err.message); }); archive.on('error', (err: Error) => { console.error('[Archiver Error]', err.message, err); if (!res.headersSent) { res.status(500).send({ error: 'Failed to create zip file: ' + err.message }); } else if (res.writable && !res.writableEnded) { res.end(); } }); archive.pipe(res); archive.directory(folderToZip, false); await archive.finalize(); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/download-zip setup:', errMsg, e); if (!res.headersSent) { let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } } }, }, { method: 'post', path: '/file/unzip', install: async ({ req, res }: FileManagerRouteParams) => { const { zipFilePath: zipFileRelativePath, destinationPath: destinationRelativePathInput, overwrite }: { zipFilePath: string; destinationPath?: string; overwrite?: boolean } = req.body; try { if (typeof zipFileRelativePath !== 'string' || zipFileRelativePath.trim() === '') { res.status(400).send('zipFilePath is required.'); return; } const absoluteZipFilePath: string = await safePathResolve(zipFileRelativePath.trim(), true); if (!(await fs.promises.stat(absoluteZipFilePath)).isFile()) { res.status(400).send(`Specified zipFilePath '${zipFileRelativePath}' is not a file.`); return; } if (!absoluteZipFilePath.toLowerCase().endsWith('.zip')) { console.warn(`[Unzip] Warning: File '${zipFileRelativePath}' may not be a .zip file based on extension. Attempting unzip.`); } let absoluteDestinationDir: string; const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir); if (typeof destinationRelativePathInput === 'string' && destinationRelativePathInput.trim() !== '') { absoluteDestinationDir = await safePathResolve(destinationRelativePathInput.trim(), false); } else { absoluteDestinationDir = path.dirname(absoluteZipFilePath); if (!absoluteDestinationDir.startsWith(resolvedCurrentBaseDir + path.sep) && absoluteDestinationDir !== resolvedCurrentBaseDir) { console.error(`[Unzip] Default destination dir "${absoluteDestinationDir}" is outside base "${resolvedCurrentBaseDir}"`); throw new Error('Default extraction path is outside the allowed project directory.'); } } try { const destStat: fs.Stats = await fs.promises.stat(absoluteDestinationDir); if (!destStat.isDirectory()) { res.status(400).send(`Destination path '${path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.'}' exists and is not a directory.`); return; } } catch (statError: any) { if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') { } else { throw statError; } } await ensureDirExists(absoluteDestinationDir); if (!overwrite) { const conflictingEntries: string[] = []; const openZipForRead = (filePath: string, options: YauzlOptions): Promise => new Promise((resolveMain, rejectMain) => { yauzl.open(filePath, options, (err: Error | null, zipfile?: YauzlZipFile) => { if (err || !zipfile) rejectMain(err || new Error("Failed to open zipfile. Path: " + filePath)); else resolveMain(zipfile); }); }); let zipfile: YauzlZipFile | null = null; try { zipfile = await openZipForRead(absoluteZipFilePath, { lazyEntries: true }); const currentZipfile: YauzlZipFile = zipfile; await new Promise((resolveLoop, rejectLoop) => { currentZipfile.on('error', rejectLoop); currentZipfile.on('end', resolveLoop); currentZipfile.on('entry', async (entry: YauzlEntry) => { const targetPath: string = path.resolve(absoluteDestinationDir, entry.fileName); if (!targetPath.startsWith(absoluteDestinationDir)) { console.warn(`[Unzip] Skipped potentially unsafe entry during conflict check: ${entry.fileName} (resolves outside destination)`); currentZipfile.readEntry(); return; } if (await pathExists(targetPath)) { const existingStat: fs.Stats = await fs.promises.stat(targetPath); const entryIsDirectory: boolean = entry.fileName.endsWith('/'); if (existingStat.isDirectory() !== entryIsDirectory) { conflictingEntries.push(`${entry.fileName} (type mismatch: existing is ${existingStat.isDirectory() ? 'dir' : 'file'}, zip entry is ${entryIsDirectory ? 'dir' : 'file'})`); } else if (!entryIsDirectory) { conflictingEntries.push(entry.fileName); } } currentZipfile.readEntry(); }); currentZipfile.readEntry(); }); } finally { zipfile?.close(); } if (conflictingEntries.length > 0) { res.status(409).send({ message: "Extraction would overwrite or conflict with existing files/directories. Please use 'overwrite: true' in your request to proceed.", conflicts: conflictingEntries }); return; } } await extract(absoluteZipFilePath, { dir: absoluteDestinationDir }); const displayDestPath: string = path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.'; res.send(`Successfully unzipped '${path.basename(zipFileRelativePath)}' to '${displayDestPath}'`); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); let statusCode: number = 500; let clientMsg: string = `Failed to unzip: ${errMsg}`; if (errMsg.includes('Project base directory is not configured')) { statusCode = 503; clientMsg = errMsg; } else if (e instanceof Error) { if (e.message.startsWith('Unsafe path')) { statusCode = 403; clientMsg = errMsg; } else if (e.message.startsWith('Path not found')) { statusCode = 404; clientMsg = errMsg; } else if (e.message.includes('is not a directory')) { statusCode = 400; clientMsg = errMsg; } else if (errMsg.toLowerCase().includes("invalid") && (errMsg.toLowerCase().includes("zip") || errMsg.toLowerCase().includes("archive"))) { statusCode = 400; clientMsg = `Failed to unzip '${zipFileRelativePath || 'file'}': May be corrupted or not a valid ZIP.`; } else if (errMsg.includes("yauzl") && (errMsg.includes("Не найден указанный файл") || errMsg.includes("end of central directory record signature not found") || errMsg.includes("Failed to open zipfile"))) { statusCode = 400; clientMsg = `Failed to read '${zipFileRelativePath || 'file'}': Invalid or corrupted ZIP file.`; } else if (e.message.includes('EEXIST') || e.message.includes('ENOTDIR') || (e.message.includes('entry') && e.message.includes('isDirectory') && e.message.includes('false but is a directory'))) { statusCode = 409; clientMsg = `Failed to unzip: A conflict occurred during extraction (e.g., a file exists where a directory was expected, or vice-versa). Original error: ${errMsg}`; } } console.error(`Error in /file/unzip (HTTP ${statusCode}):`, errMsg, e); if (!res.headersSent) res.status(statusCode).send(clientMsg); } }, }, { method: 'post', path: '/file/create', install: async ({ req, res }: FileManagerRouteParams) => { try { const fileRelativePath: string = req.body.file; if (typeof fileRelativePath !== 'string' || fileRelativePath.trim() === '') { res.status(400).send('File path is required.'); return; } const filePath: string = await safePathResolve(fileRelativePath.trim(), false); if (await pathExists(filePath)) { res.status(400).send('File or folder already exists at the target path.'); return; } await ensureDirExists(path.dirname(filePath)); await fs.promises.writeFile(filePath, ''); res.send('File created: ' + (path.relative(path.resolve(currentBaseDir), filePath).split(path.sep).join('/') || path.basename(filePath))); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/create:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/create-folder', install: async ({ req, res }: FileManagerRouteParams) => { try { const folderRelativePath: string = req.body.folder; if (typeof folderRelativePath !== 'string' || folderRelativePath.trim() === '') { res.status(400).send('Folder path is required.'); return; } const folderPath: string = await safePathResolve(folderRelativePath.trim(), false); if (await pathExists(folderPath)) { res.status(400).send('File or folder already exists at the target path.'); return; } await fs.promises.mkdir(folderPath, { recursive: true }); res.send('Folder created: ' + (path.relative(path.resolve(currentBaseDir), folderPath).split(path.sep).join('/') || path.basename(folderPath))); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/create-folder:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/open-folder', install: async ({ req, res }: FileManagerRouteParams) => { try { const relativeFolderPath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : ''; const folderPath: string = await safePathResolve(relativeFolderPath, true); if (!(await fs.promises.stat(folderPath)).isDirectory()) { res.status(400).send(`Specified path '${relativeFolderPath}' is not a directory.`); return; } const itemNames: string[] = await fs.promises.readdir(folderPath); const items = await Promise.all(itemNames.map(async (name) => { const fullPath: string = path.join(folderPath, name); const itemStat: fs.Stats = await fs.promises.stat(fullPath); return { name, isDir: itemStat.isDirectory(), size: itemStat.size, lastModified: itemStat.mtimeMs }; })); const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir); const currentDisplayPath: string = path.relative(resolvedCurrentBaseDir, folderPath).split(path.sep).join('/'); res.json({ currentPath: folderPath === resolvedCurrentBaseDir ? '' : currentDisplayPath, items, }); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/open-folder:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/rename', install: async ({ req, res }: FileManagerRouteParams) => { try { const { oldPath: oldRelativePath, newPath: newRelativePath }: { oldPath: string; newPath: string } = req.body; if (typeof oldRelativePath !== 'string' || !oldRelativePath.trim() || typeof newRelativePath !== 'string' || !newRelativePath.trim()) { res.status(400).send('Old and new paths are required and cannot be empty.'); return; } const trimmedOldRelativePath: string = oldRelativePath.trim(); const trimmedNewRelativePath: string = newRelativePath.trim(); if (trimmedOldRelativePath === trimmedNewRelativePath) { res.status(400).send('Old and new paths cannot be the same.'); return; } const oldFullPath: string = await safePathResolve(trimmedOldRelativePath, true); const newName: string = path.basename(trimmedNewRelativePath); if (!newName || newName === '.' || newName === '..') { res.status(400).send('New name is invalid.'); return; } const newParentDirRelative: string = path.dirname(trimmedNewRelativePath); const newParentDirAbsolute: string = await safePathResolve(newParentDirRelative, false); await ensureDirExists(newParentDirAbsolute); const newFullPath: string = path.join(newParentDirAbsolute, newName); const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir); if (!newFullPath.startsWith(resolvedCurrentBaseDir + path.sep) && newFullPath !== resolvedCurrentBaseDir) { throw new Error('Unsafe new path: Target is outside the allowed directory.'); } if (oldFullPath === newFullPath) { res.status(400).send('Resolved old and new paths are identical.'); return; } if (await pathExists(newFullPath)) { res.status(400).send(`Item at new path '${trimmedNewRelativePath}' already exists.`); return; } await fs.promises.rename(oldFullPath, newFullPath); res.send('Renamed successfully to ' + (path.relative(resolvedCurrentBaseDir, newFullPath).split(path.sep).join('/') || newName)); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/rename:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } }, }, { method: 'post', path: '/file/delete', install: async ({ req, res }: FileManagerRouteParams) => { try { const itemRelativePath: string = req.body.path; if (typeof itemRelativePath !== 'string' || !itemRelativePath.trim()) { res.status(400).send('Path for deletion is required.'); return; } const trimmedItemRelativePath: string = itemRelativePath.trim(); if (trimmedItemRelativePath === '.' || trimmedItemRelativePath === '/') { res.status(400).send('Invalid path for deletion. Cannot delete root.'); return; } const itemFullPath: string = await safePathResolve(trimmedItemRelativePath, true); const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir); if (itemFullPath === resolvedCurrentBaseDir) { res.status(400).send('Cannot delete the root project directory itself.'); return; } const stat: fs.Stats = await fs.promises.stat(itemFullPath); const displayPath: string = path.relative(resolvedCurrentBaseDir, itemFullPath).split(path.sep).join('/') || trimmedItemRelativePath; if (stat.isDirectory()) { await fs.promises.rm(itemFullPath, { recursive: true, force: true }); } else { await fs.promises.unlink(itemFullPath); } res.send(`${stat.isDirectory() ? 'Folder' : 'File'} deleted: ${displayPath}`); } catch (e: any) { const errMsg: string = e instanceof Error ? e.message : String(e); console.error('Error in /file/delete:', errMsg, e); let statusCode: number = 500; if (errMsg.includes('Project base directory is not configured')) statusCode = 503; else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403; else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404; res.status(statusCode).send(errMsg); } }, }, ];