const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const multer = require('multer'); const AdmZip = require('adm-zip'); const archiver = require('archiver'); const { logFileAction } = require('../db/connection'); const UPLOADS_DIR = path.join(__dirname, '../uploads'); // Helper to get safe path function getSafePath(reqPath) { const safeInfo = path.parse(reqPath); // Prevent directory traversal if (reqPath.includes('..') || path.isAbsolute(reqPath)) { // Simple sanitization, better to use path.normalize and check prefix const normalized = path.normalize(path.join(UPLOADS_DIR, reqPath)); if (!normalized.startsWith(UPLOADS_DIR)) { return null; } return normalized; } return path.join(UPLOADS_DIR, reqPath); } // Ensure uploads dir exists if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR); const storage = multer.diskStorage({ destination: function (req, file, cb) { // Support uploading to subfolders via query param or body // Multer processes before body, so we might need to send path in header or move file after // For simplicity: upload to temp then move, or just root for now and move API. // Let's rely on the query param 'path' passed to the URL let uploadPath = UPLOADS_DIR; if (req.query.path) { const safe = getSafePath(req.query.path); if (safe) uploadPath = safe; } if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true }); cb(null, uploadPath) }, filename: function (req, file, cb) { cb(null, file.originalname) } }); const upload = multer({ storage: storage }); // List Files & Folders router.get('/files', (req, res) => { const relPath = req.query.path || ''; const fullPath = getSafePath(relPath); if (!fullPath || !fs.existsSync(fullPath)) { return res.status(400).json({ error: 'Invalid path' }); } fs.readdir(fullPath, { withFileTypes: true }, (err, entries) => { if (err) return res.status(500).json({ error: 'Read error' }); const content = entries.map(entry => { const stats = fs.statSync(path.join(fullPath, entry.name)); return { name: entry.name, isDirectory: entry.isDirectory(), size: stats.size, mtime: stats.mtime, path: path.join(relPath, entry.name).replace(/\\/g, '/') }; }); // Sort folders first content.sort((a, b) => (a.isDirectory === b.isDirectory ? 0 : a.isDirectory ? -1 : 1)); res.json(content); }); }); // Upload router.post('/upload', upload.single('file'), async (req, res) => { if (!req.file) return res.status(400).send('No file'); await logFileAction(req.file.originalname, 'UPLOAD', req.session?.user?.username); res.send('Uploaded'); }); // Create Folder router.post('/folders', (req, res) => { const relPath = req.body.path || ''; const name = req.body.name; if (!name) return res.status(400).json({ error: 'Name required' }); const fullPath = getSafePath(path.join(relPath, name)); if (!fullPath) return res.status(400).json({ error: 'Invalid path' }); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); res.json({ success: true }); } else { res.status(400).json({ error: 'Exists' }); } }); // Delete Item (File or Folder) router.delete('/files', async (req, res) => { const relPath = req.query.path; const fullPath = getSafePath(relPath); if (!fullPath || !fs.existsSync(fullPath)) return res.status(404).json({ error: 'Not found' }); try { const stats = fs.statSync(fullPath); if (stats.isDirectory()) { fs.rmSync(fullPath, { recursive: true, force: true }); } else { fs.unlinkSync(fullPath); } await logFileAction(relPath, 'DELETE', req.session?.user?.username); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Download File router.get('/download', async (req, res) => { const relPath = req.query.path; const fullPath = getSafePath(relPath); if (fullPath && fs.existsSync(fullPath)) { await logFileAction(relPath, 'DOWNLOAD', req.session?.user?.username); res.download(fullPath); } else { res.status(404).send('Not Found'); } }); // ZIP Preview router.get('/zip/preview', (req, res) => { const relPath = req.query.path; const fullPath = getSafePath(relPath); if (!fullPath || !fs.existsSync(fullPath)) return res.status(404).json({ error: 'Not found' }); try { const zip = new AdmZip(fullPath); const zipEntries = zip.getEntries(); const entries = zipEntries.map(entry => ({ name: entry.entryName, isDirectory: entry.isDirectory, size: entry.header.size })); res.json(entries); } catch (e) { res.status(500).json({ error: 'Failed to read ZIP' }); } }); // Bulk Download router.post('/bulk-download', (req, res) => { const paths = req.body.paths; // Array of relative paths if (!paths || !Array.isArray(paths) || paths.length === 0) return res.status(400).send('No files'); const archive = archiver('zip', { zlib: { level: 9 } }); res.attachment('download.zip'); archive.pipe(res); paths.forEach(relPath => { const fullPath = getSafePath(relPath); if (fullPath && fs.existsSync(fullPath)) { const stats = fs.statSync(fullPath); if (stats.isDirectory()) { archive.directory(fullPath, path.basename(relPath)); } else { archive.file(fullPath, { name: path.basename(relPath) }); } } }); archive.finalize(); }); // Move/Copy router.post('/ops/move', async (req, res) => { const { source, destination } = req.body; const srcPath = getSafePath(source); const destPath = getSafePath(destination); // Destination should include the new filename/dirname if (!srcPath || !destPath || !fs.existsSync(srcPath)) return res.status(400).json({ error: 'Invalid' }); try { fs.renameSync(srcPath, destPath); await logFileAction(source, 'MOVE', req.session?.user?.username); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); router.post('/ops/copy', async (req, res) => { const { source, destination } = req.body; const srcPath = getSafePath(source); const destPath = getSafePath(destination); if (!srcPath || !destPath || !fs.existsSync(srcPath)) return res.status(400).json({ error: 'Invalid' }); try { fs.cpSync(srcPath, destPath, { recursive: true }); await logFileAction(source, 'COPY', req.session?.user?.username); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); module.exports = router;