'use strict'; const express = require('express'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const multer = require('multer'); const { db } = require('../db'); const { log } = require('../lib/audit'); const { isValidDate, legalValidity, withValidity, parseInputDate } = require('../lib/dates'); const { requireAuth, requireAdmin } = require('../middleware/auth'); const router = express.Router(); const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads'); fs.mkdirSync(UPLOAD_DIR, { recursive: true }); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => cb(null, `${Date.now()}-${crypto.randomBytes(6).toString('hex')}${path.extname(file.originalname)}`), }); const upload = multer({ storage, limits: { fileSize: 1024 * 1024 * 200 } }); // 200MB router.use(requireAuth, requireAdmin); // ---- Tree / listing --------------------------------------------------- router.get('/tree', (req, res) => { const folders = db.prepare( 'SELECT id, name, parent_id, recorded_date FROM folders ORDER BY name' ).all().map(withValidity); const files = db.prepare( 'SELECT id, name, size, mime, folder_id, created_at FROM files ORDER BY name' ).all(); res.json({ folders, files }); }); // ---- Folders ---------------------------------------------------------- router.post('/folders', (req, res) => { const { name, parent_id, recorded_date } = req.body || {}; if (!name || !name.trim()) return res.status(400).json({ error: 'Folder name required.' }); // Every folder must carry the date its material was recorded; the Legal // Validity date shown to users is derived from it. const typed = String(recorded_date || '').trim(); if (!typed) return res.status(400).json({ error: 'A recorded date is required for the folder.' }); const recorded = parseInputDate(typed); // accepts DD-MM-YYYY or YYYY-MM-DD if (!recorded) { return res.status(400).json({ error: 'Recorded date must be a valid date in DD-MM-YYYY format.' }); } const info = db.prepare( 'INSERT INTO folders (name, parent_id, recorded_date, created_by) VALUES (?, ?, ?, ?)' ).run(name.trim(), parent_id || null, recorded, req.session.user.id); log(req, 'FOLDER_CREATE', 'Data Management', `Created folder "${name}" (id ${info.lastInsertRowid}) recorded ${recorded}, valid until ${legalValidity(recorded)}`); res.json({ ok: true, id: info.lastInsertRowid, recorded_date: recorded, legal_validity: legalValidity(recorded) }); }); router.patch('/folders/:id', (req, res) => { const id = Number(req.params.id); const folder = db.prepare('SELECT * FROM folders WHERE id = ?').get(id); if (!folder) return res.status(404).json({ error: 'Folder not found.' }); const { name, parent_id, recorded_date } = req.body || {}; if (recorded_date !== undefined) { const typed = String(recorded_date || '').trim(); if (!typed) return res.status(400).json({ error: 'A recorded date is required for the folder.' }); const recorded = parseInputDate(typed); if (!recorded) { return res.status(400).json({ error: 'Recorded date must be a valid date in DD-MM-YYYY format.' }); } db.prepare('UPDATE folders SET recorded_date = ? WHERE id = ?').run(recorded, id); log(req, 'FOLDER_DATE_SET', 'Data Management', `Folder ${id} "${folder.name}" recorded date set to ${recorded} (valid until ${legalValidity(recorded)})`); } if (name !== undefined) { db.prepare('UPDATE folders SET name = ? WHERE id = ?').run(name.trim(), id); log(req, 'FOLDER_RENAME', 'Data Management', `Renamed folder ${id} "${folder.name}" -> "${name}"`); } if (parent_id !== undefined) { if (Number(parent_id) === id) return res.status(400).json({ error: 'A folder cannot contain itself.' }); // prevent moving into own descendant if (isDescendant(id, Number(parent_id))) { return res.status(400).json({ error: 'Cannot move a folder into one of its own sub-folders.' }); } db.prepare('UPDATE folders SET parent_id = ? WHERE id = ?').run(parent_id || null, id); log(req, 'FOLDER_MOVE', 'Data Management', `Moved folder ${id} "${folder.name}" to parent ${parent_id || 'root'}`); } res.json({ ok: true }); }); router.delete('/folders/:id', (req, res) => { const id = Number(req.params.id); const folder = db.prepare('SELECT * FROM folders WHERE id = ?').get(id); if (!folder) return res.status(404).json({ error: 'Folder not found.' }); // gather files under this folder subtree to remove from disk const ids = collectSubtree(id); const files = db.prepare( `SELECT stored_name FROM files WHERE folder_id IN (${ids.map(() => '?').join(',')})` ).all(...ids); db.prepare('DELETE FROM folders WHERE id = ?').run(id); // cascade removes files rows for (const f of files) safeUnlink(f.stored_name); log(req, 'FOLDER_DELETE', 'Data Management', `Deleted folder ${id} "${folder.name}" and ${files.length} file(s)`); res.json({ ok: true }); }); // ---- Files ------------------------------------------------------------ router.post('/files', upload.array('files'), (req, res) => { const folderId = req.body.folder_id ? Number(req.body.folder_id) : null; const inserted = []; const stmt = db.prepare( `INSERT INTO files (name, stored_name, size, mime, folder_id, uploaded_by) VALUES (?, ?, ?, ?, ?, ?)` ); for (const f of req.files || []) { const info = stmt.run(f.originalname, f.filename, f.size, f.mimetype, folderId, req.session.user.id); inserted.push({ id: info.lastInsertRowid, name: f.originalname }); log(req, 'FILE_UPLOAD', 'Data Management', `Uploaded "${f.originalname}" (${f.size} bytes) to folder ${folderId || 'root'}`); } res.json({ ok: true, files: inserted }); }); router.patch('/files/:id', (req, res) => { const id = Number(req.params.id); const file = db.prepare('SELECT * FROM files WHERE id = ?').get(id); if (!file) return res.status(404).json({ error: 'File not found.' }); const { name, folder_id } = req.body || {}; if (name !== undefined) { db.prepare('UPDATE files SET name = ? WHERE id = ?').run(name.trim(), id); log(req, 'FILE_RENAME', 'Data Management', `Renamed file ${id} "${file.name}" -> "${name}"`); } if (folder_id !== undefined) { db.prepare('UPDATE files SET folder_id = ? WHERE id = ?').run(folder_id || null, id); log(req, 'FILE_MOVE', 'Data Management', `Moved file ${id} "${file.name}" to folder ${folder_id || 'root'}`); } res.json({ ok: true }); }); router.delete('/files/:id', (req, res) => { const id = Number(req.params.id); const file = db.prepare('SELECT * FROM files WHERE id = ?').get(id); if (!file) return res.status(404).json({ error: 'File not found.' }); db.prepare('DELETE FROM files WHERE id = ?').run(id); safeUnlink(file.stored_name); log(req, 'FILE_DELETE', 'Data Management', `Deleted file ${id} "${file.name}"`); res.json({ ok: true }); }); // ---- helpers ---------------------------------------------------------- function childFolderIds(parentId) { return db.prepare('SELECT id FROM folders WHERE parent_id IS ?').all(parentId).map((r) => r.id); } function collectSubtree(rootId) { const out = [rootId]; const queue = [rootId]; while (queue.length) { const cur = queue.shift(); for (const cid of childFolderIds(cur)) { out.push(cid); queue.push(cid); } } return out; } function isDescendant(folderId, candidateParentId) { if (!candidateParentId) return false; return collectSubtree(folderId).includes(candidateParentId); } function safeUnlink(storedName) { try { fs.unlinkSync(path.join(UPLOAD_DIR, storedName)); } catch (_) { /* ignore */ } } module.exports = router;