fix: thumbnail content-type, animated GIF preservation, and janitor cleanup
- Add extension-based mimetype fallback in uploads route so thumbnail files serve correct Content-Type (image/webp) instead of falling back to application/octet-stream when DB lookup misses - Skip animated images (metadata.pages > 1) during thumbnail generation to preserve GIF/WebP animations instead of flattening to static frame - Remove redundant explicit thumbnail deletion in storageJanitor since deleteUploadFile() already auto-deletes the thumbnail variant
This commit is contained in:
@@ -53,26 +53,11 @@ function getDiskFiles(): DiskFile[] {
|
||||
}
|
||||
}
|
||||
|
||||
function getReferencedFilenames(): Set<string> {
|
||||
/** Filenames referenced by user/space profiles (avatars, banners, icons). */
|
||||
function getProfileReferencedFilenames(): Set<string> {
|
||||
const db = getDb();
|
||||
const referenced = new Set<string>();
|
||||
|
||||
// Attachment filenames
|
||||
const attachmentRows = db.select({ filename: schema.attachments.filename })
|
||||
.from(schema.attachments).all();
|
||||
for (const row of attachmentRows) {
|
||||
referenced.add(path.basename(row.filename));
|
||||
}
|
||||
|
||||
// Attachment thumbnails
|
||||
const thumbRows = db.select({ thumbnailFilename: schema.attachments.thumbnailFilename })
|
||||
.from(schema.attachments)
|
||||
.where(isNotNull(schema.attachments.thumbnailFilename))
|
||||
.all();
|
||||
for (const row of thumbRows) {
|
||||
if (row.thumbnailFilename) referenced.add(path.basename(row.thumbnailFilename));
|
||||
}
|
||||
|
||||
// User avatars
|
||||
const avatarRows = db.select({ avatar: schema.users.avatar })
|
||||
.from(schema.users)
|
||||
@@ -112,6 +97,30 @@ function getReferencedFilenames(): Set<string> {
|
||||
return referenced;
|
||||
}
|
||||
|
||||
/** All filenames referenced anywhere: attachments + profiles. */
|
||||
function getReferencedFilenames(): Set<string> {
|
||||
const db = getDb();
|
||||
const referenced = getProfileReferencedFilenames();
|
||||
|
||||
// Attachment filenames
|
||||
const attachmentRows = db.select({ filename: schema.attachments.filename })
|
||||
.from(schema.attachments).all();
|
||||
for (const row of attachmentRows) {
|
||||
referenced.add(path.basename(row.filename));
|
||||
}
|
||||
|
||||
// Attachment thumbnails
|
||||
const thumbRows = db.select({ thumbnailFilename: schema.attachments.thumbnailFilename })
|
||||
.from(schema.attachments)
|
||||
.where(isNotNull(schema.attachments.thumbnailFilename))
|
||||
.all();
|
||||
for (const row of thumbRows) {
|
||||
if (row.thumbnailFilename) referenced.add(path.basename(row.thumbnailFilename));
|
||||
}
|
||||
|
||||
return referenced;
|
||||
}
|
||||
|
||||
function getUnlinkedAttachments(): { id: string; filename: string; thumbnailFilename: string | null; size: number }[] {
|
||||
const db = getDb();
|
||||
const cutoff = Date.now() - UNLINKED_AGE_MS;
|
||||
@@ -210,12 +219,13 @@ export function cleanupStorage(dryRun: boolean): CleanupResult {
|
||||
const db = getDb();
|
||||
const orphans = getOrphanedFiles();
|
||||
const unlinked = getUnlinkedAttachments();
|
||||
const profileReferenced = getProfileReferencedFilenames();
|
||||
const errors: string[] = [];
|
||||
let deletedFiles = 0;
|
||||
let freedBytes = 0;
|
||||
let deletedAttachmentRecords = 0;
|
||||
|
||||
// Delete orphaned disk files
|
||||
// Phase 1: Delete orphaned disk files (not referenced by any DB record)
|
||||
for (const orphan of orphans) {
|
||||
if (!dryRun) {
|
||||
try {
|
||||
@@ -229,13 +239,15 @@ export function cleanupStorage(dryRun: boolean): CleanupResult {
|
||||
freedBytes += orphan.size;
|
||||
}
|
||||
|
||||
// Delete stale unlinked attachment records (and their disk files)
|
||||
// Phase 2: Clean up stale unlinked attachment records.
|
||||
// Files referenced by user/space profiles (avatars, banners, icons) are
|
||||
// preserved on disk — only the orphaned attachment DB record is removed.
|
||||
for (const att of unlinked) {
|
||||
const fileInUseByProfile = profileReferenced.has(path.basename(att.filename));
|
||||
if (!dryRun) {
|
||||
try {
|
||||
deleteUploadFile(att.filename);
|
||||
if (att.thumbnailFilename) {
|
||||
deleteUploadFile(att.thumbnailFilename);
|
||||
if (!fileInUseByProfile) {
|
||||
deleteUploadFile(att.filename);
|
||||
}
|
||||
db.delete(schema.attachments)
|
||||
.where(eq(schema.attachments.id, att.id))
|
||||
@@ -246,7 +258,9 @@ export function cleanupStorage(dryRun: boolean): CleanupResult {
|
||||
}
|
||||
}
|
||||
deletedAttachmentRecords++;
|
||||
freedBytes += att.size;
|
||||
if (!fileInUseByProfile) {
|
||||
freedBytes += att.size;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user