feat: pre-generate Directus image previews
Bundle extension (upload hook + API endpoint + admin module) that warms Directus image transform cache in-process with an explicit acceptFormat, so the generated file matches the browser's cache key byte-for-byte. - resumable, window-aware backfill with progress + cancel - system + custom presets, effective-format reporting, shadow warnings - storage breakdown (previews vs originals), human-readable API fields - diagnostics: resolve-plan prediction, per-file generate, ops ring buffer - isolated dev/staging compose stack + deploy/rollback scripts Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed, idempotent backfill, custom fixed-format presets, error surfacing.
This commit is contained in:
commit
c8f8bbc5ac
42 changed files with 10005 additions and 0 deletions
56
src/hook/index.ts
Normal file
56
src/hook/index.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { defineHook } from '@directus/extensions-sdk';
|
||||
import { recordOp } from '../lib/diagnostics';
|
||||
import { buildRegistry, isImageType, loadCustomPresets } from '../lib/presets';
|
||||
import { readSettings } from '../lib/settings';
|
||||
import { warmFile } from '../lib/warmer';
|
||||
|
||||
interface UploadMeta {
|
||||
key?: string;
|
||||
collection?: string;
|
||||
payload?: { type?: string | null; collection?: string };
|
||||
}
|
||||
|
||||
export default defineHook(({ action }, { services, getSchema, env, logger }) => {
|
||||
action('files.upload', (meta) => {
|
||||
void warmUpload(meta as UploadMeta).catch((error) =>
|
||||
logger.error(error, 'image-previews: upload warm 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,
|
||||
}),
|
||||
});
|
||||
|
||||
if (settings.debug) logger.info({ fileId, summary }, 'image-previews: warmed upload');
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue