Remove orphaned upload files
This commit is contained in:
+116
@@ -339,6 +339,108 @@ function createUploadMiddleware(uploadDir) {
|
|||||||
return multer({ storage });
|
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) {
|
function parseCookies(cookieHeader) {
|
||||||
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
|
return String(cookieHeader || '').split(/;\s*/).reduce(function (cookies, pair) {
|
||||||
if (!pair) {
|
if (!pair) {
|
||||||
@@ -1448,12 +1550,17 @@ async function start() {
|
|||||||
return res.status(404).send('Slide not found');
|
return res.status(404).send('Slide not found');
|
||||||
}
|
}
|
||||||
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
|
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
|
||||||
|
const existingUploadRefs = collectUploadReferencesFromSlide(slide);
|
||||||
const payload = await common.buildSlidePayload(pool, req, slide);
|
const payload = await common.buildSlidePayload(pool, req, slide);
|
||||||
|
const nextUploadRefs = collectUploadReferencesFromPayload(payload);
|
||||||
const actorId = getAuditUserId(req);
|
const actorId = getAuditUserId(req);
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'UPDATE slides SET title = ?, body = ?, template_id = ?, content_json = ?, media_path = ?, media_type = ?, modified_by = ? WHERE id = ?',
|
'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]
|
[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 notifyPlayerScreens(affectedScreens, 'refresh');
|
||||||
await broadcastDashboardState();
|
await broadcastDashboardState();
|
||||||
res.redirect('/admin/slides/' + slide.id + '/edit?message=' + encodeURIComponent('Slide updated.'));
|
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');
|
return res.status(404).send('Slide not found');
|
||||||
}
|
}
|
||||||
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
|
const affectedScreens = await fetchScreensBySlideId(pool, slide.id);
|
||||||
|
const uploadRefs = collectUploadReferencesFromSlide(slide);
|
||||||
await pool.query('DELETE FROM slides WHERE id = ?', [slide.id]);
|
await pool.query('DELETE FROM slides WHERE id = ?', [slide.id]);
|
||||||
|
await removeUnusedUploadFiles(pool, UPLOAD_DIR, Array.from(uploadRefs));
|
||||||
await notifyPlayerScreens(affectedScreens, 'refresh');
|
await notifyPlayerScreens(affectedScreens, 'refresh');
|
||||||
await broadcastDashboardState();
|
await broadcastDashboardState();
|
||||||
res.redirect('/admin/slides?message=' + encodeURIComponent('Slide deleted.'));
|
res.redirect('/admin/slides?message=' + encodeURIComponent('Slide deleted.'));
|
||||||
@@ -1536,12 +1645,17 @@ async function start() {
|
|||||||
if (!template) {
|
if (!template) {
|
||||||
return res.status(404).send('Template not found');
|
return res.status(404).send('Template not found');
|
||||||
}
|
}
|
||||||
|
const existingUploadRefs = collectUploadReferencesFromTemplate(template);
|
||||||
const payload = await common.buildTemplatePayload(pool, req, template);
|
const payload = await common.buildTemplatePayload(pool, req, template);
|
||||||
|
const nextUploadRefs = collectUploadReferencesFromPayload(payload);
|
||||||
const actorId = getAuditUserId(req);
|
const actorId = getAuditUserId(req);
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'UPDATE slide_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, modified_by = ? WHERE id = ?',
|
'UPDATE slide_templates SET name = ?, canvas_size_id = ?, background_image_path = ?, modified_by = ? WHERE id = ?',
|
||||||
[payload.name, payload.canvasSizeId, payload.backgroundImagePath, actorId, template.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]);
|
await pool.query('DELETE FROM slide_template_regions WHERE template_id = ?', [template.id]);
|
||||||
for (let i = 0; i < payload.regions.length; i += 1) {
|
for (let i = 0; i < payload.regions.length; i += 1) {
|
||||||
const region = payload.regions[i];
|
const region = payload.regions[i];
|
||||||
@@ -1562,9 +1676,11 @@ async function start() {
|
|||||||
if (!template) {
|
if (!template) {
|
||||||
return res.status(404).send('Template not found');
|
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('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_template_regions WHERE template_id = ?', [template.id]);
|
||||||
await pool.query('DELETE FROM slide_templates WHERE 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.'));
|
res.redirect('/admin/templates?message=' + encodeURIComponent('Template deleted.'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user