Files
picpeak/backend/src/services/downloadJobCleanupService.js
T
Paul Nothaft 8e3573788b 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.
2026-08-11 09:46:46 +02:00

44 lines
1.4 KiB
JavaScript

/**
* downloadJobCleanupService — TTL sweep for custom-resolution download jobs
* (#858).
*
* Job archives are one-off renditions of a gallery at a size nothing else
* caches against, so they are pure disposable bytes: once the TTL passes the
* row and its zip go away. Without this sweep, every guest who ever picked a
* non-standard resolution would leave a full gallery-sized archive behind in
* `.download-cache` forever.
*
* Runs every 20 minutes, offset from the hourly jobs so the three cleanup
* schedulers don't all wake at once.
*/
const cron = require('node-cron');
const logger = require('../utils/logger');
const downloadJobService = require('./downloadJobService');
function startDownloadJobCleanup() {
// A restart leaves any in-flight build with no worker. Fail those rows once
// at startup so their owners get a clear error instead of polling forever.
downloadJobService.recoverOrphanedJobs().catch((err) =>
logger.error('Download job recovery failed', { error: err.message }));
cron.schedule('7,27,47 * * * *', async () => {
await runDownloadJobCleanup();
});
logger.info('Download job cleanup scheduler started');
}
async function runDownloadJobCleanup() {
try {
await downloadJobService.sweepExpired();
} catch (err) {
logger.error('Download job cleanup failed', { error: err.message });
}
}
module.exports = {
startDownloadJobCleanup,
// exported for tests / manual invocation
runDownloadJobCleanup,
};