v_1.5_patch_0.2
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user