fix: rekursive Ordner-Löschung, Multer-Cleanup, Waisen-Bereinigung (Disk+DB)

This commit is contained in:
2026-06-16 13:19:29 +02:00
parent 57f6badde5
commit f5c9628035
3 changed files with 204 additions and 9 deletions

View File

@@ -319,4 +319,69 @@ router.post('/webdav/sync', authenticate, requireAdmin, async (req, res) => {
});
// ── Waisen aufräumen ──────────────────────────────────────────────────────
// Findet und löscht:
// 1. Dateien auf Disk die nicht in der DB sind (Disk-Waisen)
// 2. DB-Einträge die keine Datei auf Disk haben (DB-Waisen)
router.post('/cleanup-orphans', authenticate, requireAdmin, (req, res) => {
const { dry_run = false } = req.body;
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
const path = require('path');
const fs = require('fs');
const results = {
disk_orphans: [], // auf Disk aber nicht in DB
db_orphans: [], // in DB aber nicht auf Disk
disk_deleted: 0,
db_deleted: 0,
};
try {
// Alle Dateinamen aus DB
const dbFiles = db.prepare('SELECT id, filename, originalname, size FROM files').all();
const dbFilenames = new Set(dbFiles.map(f => f.filename));
// Alle Dateien auf Disk
const diskFiles = fs.readdirSync(UPLOAD_DIR).filter(f => !f.startsWith('.'));
// 1. Disk-Waisen: auf Disk aber nicht in DB
for (const diskFile of diskFiles) {
if (!dbFilenames.has(diskFile)) {
const fullPath = path.join(UPLOAD_DIR, diskFile);
const stat = fs.statSync(fullPath);
results.disk_orphans.push({ filename: diskFile, size: stat.size });
if (!dry_run) {
try { fs.unlinkSync(fullPath); results.disk_deleted++; } catch(e) {
console.error('[cleanup] disk unlink failed:', diskFile, e.message);
}
}
}
}
// 2. DB-Waisen: in DB aber nicht auf Disk
const diskFileset = new Set(diskFiles);
for (const dbFile of dbFiles) {
if (!diskFileset.has(dbFile.filename)) {
results.db_orphans.push({ id: dbFile.id, filename: dbFile.filename, originalname: dbFile.originalname });
if (!dry_run) {
db.prepare('DELETE FROM files WHERE id=?').run(dbFile.id);
results.db_deleted++;
}
}
}
res.json({
dry_run,
disk_orphans_count: results.disk_orphans.length,
db_orphans_count: results.db_orphans.length,
disk_deleted: results.disk_deleted,
db_deleted: results.db_deleted,
disk_orphans: results.disk_orphans.slice(0, 50),
db_orphans: results.db_orphans.slice(0, 50),
});
} catch(err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;

View File

@@ -146,16 +146,33 @@ router.post('/folders', authenticate, (req, res) => {
});
router.delete('/folders/:id', authenticate, (req, res) => {
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, req.user.id);
const uid = req.user.id;
const folder = db.prepare('SELECT * FROM folders WHERE id=? AND user_id=?').get(req.params.id, uid);
if (!folder) return res.status(404).json({ error: 'Nicht gefunden' });
// Delete all files inside
const files = db.prepare('SELECT * FROM files WHERE folder_id=? AND user_id=?').all(folder.id, req.user.id);
for (const f of files) { try { fs.unlinkSync(path.join(UPLOAD_DIR, f.filename)); } catch {} }
// Alle Unterordner rekursiv sammeln
function collectSubfolderIds(parentId) {
const children = db.prepare('SELECT id FROM folders WHERE parent_id=? AND user_id=?').all(parentId, uid);
let ids = [parentId];
for (const c of children) ids = ids.concat(collectSubfolderIds(c.id));
return ids;
}
const allFolderIds = collectSubfolderIds(folder.id);
// WebDAV: Ordner löschen (vor DB-Löschung damit Pfad noch auflösbar)
const _davFolderDel = davFolderUrl(getUserName(req.user.id), folder.id);
const _davFolderDel = davFolderUrl(getUserName(uid), folder.id);
if (_davFolderDel) webdav.deleteFile(_davFolderDel).catch(e => console.warn('[webdav] rmdir:', e.message));
db.prepare('DELETE FROM files WHERE folder_id=? AND user_id=?').run(folder.id, req.user.id);
db.prepare('DELETE FROM folders WHERE id=?').run(folder.id);
// Alle Dateien in allen Unterordnern löschen (Disk + DB)
for (const fid of allFolderIds) {
const files = db.prepare('SELECT * FROM files WHERE folder_id=? AND user_id=?').all(fid, uid);
for (const f of files) { try { fs.unlinkSync(path.join(UPLOAD_DIR, f.filename)); } catch {} }
db.prepare('DELETE FROM files WHERE folder_id=? AND user_id=?').run(fid, uid);
}
// Alle Unterordner + Hauptordner aus DB löschen
for (const fid of allFolderIds.reverse()) {
db.prepare('DELETE FROM folders WHERE id=?').run(fid);
}
res.json({ success: true });
});
@@ -236,7 +253,10 @@ router.post('/', authenticate, (req, res) => {
if (usedBytes >= totalLimitMb * 1024 * 1024)
return res.status(400).json({ error: `Gesamtlimit von ${totalLimitMb} MB erreicht` });
getUpload().single('file')(req, res, err => {
if (err) return res.status(400).json({ error: err.message });
if (err) {
if (req.file) try { fs.unlinkSync(req.file.path); } catch {}
return res.status(400).json({ error: err.message });
}
if (!req.file) return res.status(400).json({ error: 'Keine Datei' });
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
@@ -295,7 +315,10 @@ const upload3d = multer({
router.post('/upload-3d', authenticate, (req, res) => {
upload3d.array('files', 20)(req, res, err => {
if (err) return res.status(400).json({ error: err.message });
if (err) {
if (req.files?.length) req.files.forEach(f => { try { fs.unlinkSync(f.path); } catch {} });
return res.status(400).json({ error: err.message });
}
if (!req.files?.length) return res.status(400).json({ error: 'Keine Dateien' });
const uid = req.user.id;
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;