Remove orphaned upload files
Publish Docker Image / build-and-push (push) Successful in 20s

This commit is contained in:
2026-07-14 14:30:28 +01:00
parent ce9ecffa4a
commit 41b619a16a
+116
View File
@@ -339,6 +339,108 @@ function createUploadMiddleware(uploadDir) {
return multer({ storage });
}
function normalizeUploadReference(uploadPath) {
const value = String(uploadPath || '').trim();
if (!value || !value.startsWith('/uploads/')) {
return null;
}
return value;
}
function collectUploadReferencesFromValue(value, refs) {
if (!value) {
return refs;
}
const stack = [value];
while (stack.length) {
const current = stack.pop();
if (Array.isArray(current)) {
current.forEach(function (item) {
stack.push(item);
});
continue;
}
if (current && typeof current === 'object') {
Object.keys(current).forEach(function (key) {
stack.push(current[key]);
});
continue;
}
if (typeof current === 'string') {
const reference = normalizeUploadReference(current);
if (reference) {
refs.add(reference);
}
}
}
return refs;
}
function collectUploadReferencesFromSlide(slide) {
const refs = new Set();
if (!slide) {
return refs;
}
collectUploadReferencesFromValue(slide.media_path, refs);
collectUploadReferencesFromValue(common.parseJsonSafe(slide.content_json), refs);
return refs;
}
function collectUploadReferencesFromTemplate(template) {
const refs = new Set();
if (!template) {
return refs;
}
collectUploadReferencesFromValue(template.background_image_path, refs);
return refs;
}
function collectUploadReferencesFromPayload(payload) {
const refs = new Set();
if (!payload) {
return refs;
}
collectUploadReferencesFromValue(payload.mediaPath, refs);
collectUploadReferencesFromValue(common.parseJsonSafe(payload.contentJson), refs);
collectUploadReferencesFromValue(payload.backgroundImagePath, refs);
return refs;
}
async function countUploadReferences(pool, uploadPath) {
const [slideRows] = await pool.query(
`SELECT COUNT(*) AS ref_count
FROM slides
WHERE media_path = ?
OR JSON_SEARCH(COALESCE(content_json, JSON_OBJECT()), 'one', ?) IS NOT NULL`,
[uploadPath, uploadPath]
);
const [templateRows] = await pool.query(
'SELECT COUNT(*) AS ref_count FROM slide_templates WHERE background_image_path = ?',
[uploadPath]
);
return Number(slideRows[0].ref_count || 0) + Number(templateRows[0].ref_count || 0);
}
async function removeUnusedUploadFiles(pool, uploadDir, uploadPaths) {
const uniquePaths = Array.from(new Set((uploadPaths || []).map(normalizeUploadReference).filter(Boolean)));
for (let i = 0; i < uniquePaths.length; i += 1) {
const uploadPath = uniquePaths[i];
const referenceCount = await countUploadReferences(pool, uploadPath);
if (referenceCount > 0) {
continue;
}
const filePath = path.join(uploadDir, path.basename(uploadPath));
try {
await fs.promises.unlink(filePath);
} catch (error) {
if (error && error.code !== 'ENOENT') {
console.warn('Unable to remove unused upload file:', filePath, error);
}
}
}
}
function parseCookies(cookieHeader) {
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
if (!pair) {
@@ -1448,12 +1550,17 @@ async function start() {
return res.status(404).send('Slide not found');
}
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
const existingUploadRefs = collectUploadReferencesFromSlide(slide);
const payload = await common.buildSlidePayload(pool, req, slide);
const nextUploadRefs = collectUploadReferencesFromPayload(payload);
const actorId = getAuditUserId(req);
await pool.query(
'UPDATE slides SET title = ?, body = ?, template_id = ?, content_json = ?, media_path = ?, media_type = ?, modified_by = ? WHERE id = ?',
[payload.title, payload.body, payload.templateId, payload.contentJson, payload.mediaPath, payload.mediaType, actorId, slide.id]
);
await removeUnusedUploadFiles(pool, UPLOAD_DIR, Array.from(existingUploadRefs).filter(function (reference) {
return !nextUploadRefs.has(reference);
}));
await notifyPlayerScreens(affectedScreens, 'refresh');
await broadcastDashboardState();
res.redirect('/admin/slides/' + slide.id + '/edit?message=' + encodeURIComponent('Slide updated.'));
@@ -1469,7 +1576,9 @@ async function start() {
return res.status(404).send('Slide not found');
}
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
const uploadRefs = collectUploadReferencesFromSlide(slide);
await pool.query('DELETE FROM slides WHERE id = ?', [slide.id]);
await removeUnusedUploadFiles(pool, UPLOAD_DIR, Array.from(uploadRefs));
await notifyPlayerScreens(affectedScreens, 'refresh');
await broadcastDashboardState();
res.redirect('/admin/slides?message=' + encodeURIComponent('Slide deleted.'));
@@ -1536,12 +1645,17 @@ async function start() {
if (!template) {
return res.status(404).send('Template not found');
}
const existingUploadRefs = collectUploadReferencesFromTemplate(template);
const payload = await common.buildTemplatePayload(pool, req, template);
const nextUploadRefs = collectUploadReferencesFromPayload(payload);
const actorId = getAuditUserId(req);
await pool.query(
'UPDATE slide_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, modified_by = ? WHERE id = ?',
[payload.name, payload.canvasSizeId, payload.backgroundImagePath, actorId, template.id]
);
await removeUnusedUploadFiles(pool, UPLOAD_DIR, Array.from(existingUploadRefs).filter(function (reference) {
return !nextUploadRefs.has(reference);
}));
await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]);
for (let i = 0; i < payload.regions.length; i += 1) {
const region = payload.regions[i];
@@ -1562,9 +1676,11 @@ async function start() {
if (!template) {
return res.status(404).send('Template not found');
}
const uploadRefs = collectUploadReferencesFromTemplate(template);
await pool.query('UPDATE slides SET template_id = NULL, modified_by = ? WHERE template_id = ?', [getAuditUserId(req), template.id]);
await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]);
await pool.query('DELETE FROM slide_templates WHERE id = ?', [template.id]);
await removeUnusedUploadFiles(pool, UPLOAD_DIR, Array.from(uploadRefs));
res.redirect('/admin/templates?message=' + encodeURIComponent('Template deleted.'));
} catch (error) {
next(error);