import { defineHook } from '@directus/extensions-sdk'; import { runBackfill } from '../lib/backfill'; import { recordOp } from '../lib/diagnostics'; import { replayFailureMap } from '../lib/failures'; import { buildRegistry, isImageType, loadCustomPresets } from '../lib/presets'; import { journalFailure, markRecovered, runDueRetries } from '../lib/retry'; import { readSettings } from '../lib/settings'; import { readRun } from '../lib/state'; import { warmFile } from '../lib/warmer'; import { isInWindow } from '../lib/window'; interface UploadMeta { key?: string; collection?: string; payload?: { type?: string | null; collection?: string }; } export default defineHook(({ action, schedule }, { services, database, getSchema, env, logger }) => { action('files.upload', (meta) => { void warmUpload(meta as UploadMeta).catch((error) => logger.error(error, 'image-previews: upload warm failed'), ); }); schedule('*/15 * * * *', () => { void scheduledBackfill().catch((error) => logger.error(error, 'image-previews: scheduled run failed'), ); }); async function warmUpload(meta: UploadMeta): Promise { const settings = await readSettings(env); if (!settings.enabled || !settings.onUpload) return; const fileId = meta?.key; const payload = meta?.payload; if (!fileId || !payload) return; if (payload.collection && payload.collection !== 'directus_files') return; if (!isImageType(payload.type)) return; const schema = await getSchema(); const custom = await loadCustomPresets(new services['SettingsService']({ schema })); const presets = buildRegistry(custom, settings.presetKeys); if (presets.length === 0) return; const assets = new services['AssetsService']({ schema }); const summary = await warmFile(assets, fileId, presets, { concurrency: settings.concurrency, formats: settings.formats, onResult: (result) => recordOp({ ts: Date.now(), fileId, key: result.key, format: result.format, ok: result.ok, ms: result.ms, error: result.error, }), }); const failureRecords = await replayFailureMap(env); for (const result of summary.results) { if (result.ok) { await markRecovered( env, { file_id: fileId, preset_key: result.key, format: result.format, ms: result.ms, }, { records: failureRecords, logger }, ); } else { await journalFailure( env, { file_id: fileId, preset_key: result.key, format: result.format, error: result.error, ms: result.ms, }, { records: failureRecords, logger }, ); } } if (settings.debug) logger.info({ fileId, summary }, 'image-previews: warmed upload'); } async function scheduledBackfill(): Promise { const settings = await readSettings(env); if (!settings.enabled) return; if (!isInWindow(settings.windowStart, settings.windowEnd)) return; const run = await readRun(env); if (run?.running) return; const retryOutcome = await runDueRetries({ services, database, getSchema, env, logger }, {}); if (retryOutcome.status === 'completed' && retryOutcome.processed > 0) { logger.info(retryOutcome, 'image-previews: due retries processed'); } const finishedToday = run?.finishedAt ? new Date(run.finishedAt).toDateString() === new Date().toDateString() : false; if (finishedToday && run && run.done >= run.total) return; logger.info('image-previews: scheduled backfill starting'); void runBackfill({ services, database, getSchema, env, logger }, {}).catch((error) => logger.error(error, 'image-previews: scheduled backfill failed'), ); } });