directus-extension-generate.../src/hook/index.ts

125 lines
3.6 KiB
TypeScript

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<void> {
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<void> {
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'),
);
}
});