feat: add age-based media cleanup endpoint

Adds cleanupOldMedia() to the storage janitor and a new endpoint
POST /api/admin/storage/cleanup-media { maxAgeDays, dryRun }.
Deletes chat attachments older than the specified threshold while
preserving profile images.
This commit is contained in:
Jannis Braun
2026-03-23 02:20:28 +01:00
parent eb743ca2ca
commit ee11408060
3 changed files with 59 additions and 1 deletions
@@ -339,3 +339,43 @@ export function cleanupStorage(dryRun: boolean): CleanupResult {
errors,
};
}
export function cleanupOldMedia(maxAgeDays: number, dryRun: boolean): CleanupResult {
const db = getDb();
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
const profileReferenced = getProfileReferencedFilenames();
const errors: string[] = [];
let deletedFiles = 0;
let freedBytes = 0;
let deletedAttachmentRecords = 0;
// Find message attachments older than the cutoff
const rawDb = getRawDb();
const oldMedia = rawDb.prepare(`
SELECT id, filename, size FROM attachments
WHERE (message_id IS NOT NULL OR dm_message_id IS NOT NULL)
AND created_at < ?
`).all(cutoff) as { id: string; filename: string; size: number }[];
for (const att of oldMedia) {
// Never delete files currently used as profile images
if (profileReferenced.has(path.basename(att.filename))) continue;
if (!dryRun) {
try {
deleteUploadFile(att.filename);
db.delete(schema.attachments)
.where(eq(schema.attachments.id, att.id))
.run();
} catch (err: any) {
errors.push(`Failed to delete old media ${att.id}: ${err.message}`);
continue;
}
}
deletedFiles++;
freedBytes += att.size;
deletedAttachmentRecords++;
}
return { dryRun, deletedFiles, freedBytes, deletedAttachmentRecords, errors };
}