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.
102 lines
6 KiB
Markdown
102 lines
6 KiB
Markdown
# directus-extension-generate-image-previews
|
||
|
||
Pre-generate Directus image transform ("preview") cache files so the admin UI and API never pay the
|
||
first-request sharp cost.
|
||
|
||
A Directus **bundle**:
|
||
|
||
- **upload hook** — warm previews right after an image upload.
|
||
- **new-preset trigger** — `Run now` warms every preset (system + custom) for existing images.
|
||
- **backfill job** — resumable, window-aware sweep over the existing library.
|
||
- **admin module** — progress, storage breakdown, configuration, diagnostics, and API reference.
|
||
|
||
Target: self-hosted Directus **11.17.4** (PM2 cluster, local disk storage).
|
||
|
||
## Why this approach (read AGENTS.md too)
|
||
|
||
The cache file is `<filename_disk>__<object-hash(resolved transforms)>.<ext>`. The hash depends on the
|
||
RESOLVED transforms, and every `system-*` preset is `format:'auto'` — which resolves differently
|
||
server-side (jpg/png) than in a browser (avif). We therefore generate **in-process** with an explicit
|
||
`acceptFormat` (`avif` + `webp`), reproducing the browser's cache file byte-for-byte without any HTTP
|
||
request. Naive warming (or the popular community extension) silently misses.
|
||
|
||
## Install / build / publish
|
||
|
||
```bash
|
||
npm install
|
||
npm run build # emits dist/app.js + dist/api.js
|
||
npm run typecheck # 0 errors
|
||
npm test # unit tests
|
||
npm run dev # watch build
|
||
npm run publish # scp dist + package.json to the (staging) deploy target
|
||
```
|
||
|
||
## Settings
|
||
|
||
Defaults are applied automatically; everything is editable at runtime from the admin module — no rebuild.
|
||
|
||
| setting | default | meaning |
|
||
| -------------------------- | -------------------------------------- | ---------------------------------------------------------------- |
|
||
| `enabled` | `true` | master switch (off = hooks/backfill inert) |
|
||
| `onUpload` | `true` | generate previews immediately after an upload |
|
||
| `windowStart` / `windowEnd` | `01:00` / `04:00` | scheduled runs only start inside this window (local time) |
|
||
| `concurrency` | `1` | parallel generations |
|
||
| `formats` | `avif,webp` | output variants **for `format:'auto'` presets only** (all system presets) |
|
||
| `presetKeys` | `null` (all) | restrict to specific preset keys |
|
||
| `debug` | `false` | verbose generation logs in the Directus server log |
|
||
| `IMAGE_PREVIEWS_STATE_DIR` | `<STORAGE_LOCAL_ROOT>/.image-previews` | where settings + progress state live |
|
||
|
||
### Presets and formats
|
||
|
||
- **System presets** (`system-small|medium|large` × `cover|contain`) always use `format:'auto'`, so they
|
||
generate one file per entry in `formats` (default avif + webp), at their built-in sizes (64 / 300 / 800 px).
|
||
- **Custom presets** from Settings → File Storage are added automatically. A custom preset with a fixed
|
||
`format` (e.g. `png`) ignores `formats` and generates exactly that format. A custom preset with
|
||
`format:'auto'` follows `formats`.
|
||
- A custom preset whose `key` equals a system key is **shadowed** (system wins, same as Directus) and is
|
||
flagged in the admin module.
|
||
|
||
## Storage reporting
|
||
|
||
`GET /image-previews/disk-usage` (and the admin panel) reports bytes + file counts separately for
|
||
**preview assets** and **original assets**, plus the total — in both raw bytes and human-readable form.
|
||
|
||
## API endpoints
|
||
|
||
All require an admin session or admin token. Human-readable fields accompany machine-readable ones.
|
||
|
||
- `GET /image-previews/status` — run progress, rate, ETA, window state, ISO + human dates
|
||
- `GET /image-previews/config`, `PUT /image-previews/config`
|
||
- `GET /image-previews/presets` — system + custom presets, effective formats, shadowed keys
|
||
- `POST /image-previews/run` — start backfill (`{"ignoreWindow":true}` for an immediate run)
|
||
- `POST /image-previews/run/cancel`
|
||
- `GET /image-previews/disk-usage` — previews vs originals vs total
|
||
- `POST /image-previews/generate` — one file (`{fileId, keys?, formats?, dryRun?}`)
|
||
- `GET /image-previews/resolve-plan?fileId=&key=` — predicted cache filename, no write
|
||
- `GET /image-previews/ops?limit=` — recent operations with timing + error messages
|
||
|
||
The admin module includes a full API reference with curl examples.
|
||
|
||
## Safe testing & deployment
|
||
|
||
Never test against the prod instance.
|
||
|
||
- **Local / staging** (`dev/compose.dev.yml`): Directus 11.17.4 + its own postgres + volumes, on separate
|
||
ports. Shares nothing with prod. See `dev/README.md`.
|
||
- **Fail-open**: entrypoints never throw into Directus core.
|
||
- **Rollout**: build → verify on staging → back up previous `dist/` → deploy → healthcheck → enable.
|
||
**Rollback**: restore previous `dist/`.
|
||
- Scripts: `dev/deploy-staging.sh`, `dev/rollback-staging.sh`, template `dev/compose.staging.yml`.
|
||
|
||
## Caveats
|
||
|
||
- **Transform dimension guard, not a file-size limit.** Directus rejects transformations of images whose
|
||
width or height exceeds `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` (default **6000 px**). This is a Directus
|
||
env var, not something this extension imposes. To process larger images, raise it on the Directus
|
||
instance (and watch memory). Upload size limits (`FILES_MAX_UPLOAD_SIZE`, `MAX_PAYLOAD_SIZE`) are separate.
|
||
- Warm only after uploads are committed (local writes are non-atomic).
|
||
- In-process generation bypasses the HTTP rate limiter; bound it with `concurrency` + the window.
|
||
- Unsupported custom preset formats (e.g. `bmp`) fail at generation time; the error appears in
|
||
Recent operations / `/ops`.
|
||
|
||
See `.opencode/goal.md` for the full contract and `.opencode/feature_list.json` for the task list.
|