feat: optimize profile image sizes, silent PWA updates, storage cleanup fixes

- Resize avatars/icons to 256px and banners to 1280px (client crop + server safety net)
- Add server-side resizeProfileImage() for federation/API uploads without crop modal
- Fix unconstrained crop on RegisterPage and CreateSpace (was missing maxOutputDimension)
- PWA: switch to autoUpdate with skipWaiting/clientsClaim for seamless deploys
- Storage janitor: exclude profile images from unlinked cleanup, delete stale thumbnails
- Add deleteAttachmentByFilename() to clean orphaned attachment records for profile images
- Migration: one-time cleanup of stale profile image attachment records
- GeneralPanel: wrap in <form> to prevent implicit submission
This commit is contained in:
Jannis Braun
2026-03-15 19:16:48 +01:00
parent 4d230711fc
commit 2e6fa3cdc6
15 changed files with 250 additions and 30 deletions
+31
View File
@@ -1,6 +1,37 @@
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
const PROFILE_LIMITS = { avatar: 256, icon: 256, banner: 1280 } as const;
/**
* Resize a profile image (avatar, icon, or banner) to the target max dimension.
* Preserves animated GIF frames. No-op if the image is already small enough.
* Writes to a temp file then atomically renames to avoid corruption.
*/
export async function resizeProfileImage(
filepath: string,
type: 'avatar' | 'icon' | 'banner',
): Promise<void> {
const maxDim = PROFILE_LIMITS[type];
try {
const image = sharp(filepath, { animated: true });
const metadata = await image.metadata();
if (!metadata.width || metadata.width <= maxDim) return;
const tmpPath = filepath + '.tmp';
await sharp(filepath, { animated: true })
.resize({ width: maxDim, withoutEnlargement: true })
.toFile(tmpPath);
fs.renameSync(tmpPath, filepath);
} catch (err) {
// Non-fatal — the original file is still intact
console.error(`Profile image resize failed (non-fatal) for ${path.basename(filepath)}:`, err);
// Clean up temp file if it was partially written
try { fs.unlinkSync(filepath + '.tmp'); } catch { /* ignore */ }
}
}
const RESIZABLE_MIMETYPES = new Set([
'image/jpeg',
'image/png',