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.
44 lines
1.4 KiB
JavaScript
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,
|
|
};
|