feat(downloads): per-gallery download resolutions (#858) (#1022)

Clients who need smaller files no longer make the photographer re-export. Two capabilities, both off by default.

STANDARD RESOLUTION — the size a gallery hands out for every ordinary download (single, selected, download-all). Global default in Settings, overridable per gallery with the NULL=inherit tri-state. The pre-built download-all zip is built AT the standard resolution, so changing it invalidates those archives, including a fan-out to inheriting galleries.

RESOLUTION PICKER — opt-in modal letting guests choose a different size. Custom archives are built as a DB-backed job the client polls, never cached. The picker never offers a size above the standard, and Original reappears only when the admin explicitly allows it.

Resize is fit:'inside' + withoutEnlargement — aspect preserved, never upscaled — applied before the watermark, since the mark is sized relative to its input.

Three rounds of external review hardened this: job archives are bound to the requester's visibility scope and re-validated at delivery, the streamed download-all path applies the cap, queue admission is bounded, and rejected resolutions no longer inflate download stats.

Closes #858.
This commit is contained in:
Paul Nothaft
2026-08-11 09:46:46 +02:00
committed by GitHub
parent 02deac9f10
commit 8e3573788b
29 changed files with 2716 additions and 82 deletions
+4
View File
@@ -21,6 +21,7 @@ const { initializeDatabase, db } = require('./src/database/db');
const { startFileWatcher } = require('./src/services/fileWatcher');
const { startExpirationChecker } = require('./src/services/expirationChecker');
const { startTransferCleanup } = require('./src/services/transferCleanupService');
const { startDownloadJobCleanup } = require('./src/services/downloadJobCleanupService');
const { startRevealScheduler } = require('./src/services/revealScheduler');
const { startInvoiceScheduler } = require('./src/services/invoiceSchedulerService');
const { initializeTransporter, startEmailQueueProcessor } = require('./src/services/emailProcessor');
@@ -912,6 +913,9 @@ async function startServer() {
// PicTransfer retention sweep (#997): expire links, notify admins, and
// hard-delete client uploads once the grace window elapses.
startTransferCleanup();
// Custom-resolution download archives (#858) are disposable renditions —
// sweep them once their TTL passes so .download-cache doesn't grow forever.
startDownloadJobCleanup();
// Reveal-mode scheduler (#838): minutely stamp for scheduled reveals.
startRevealScheduler();
// CRM invoice scheduler: hourly tick to flush scheduled-send invoices