directus-extension-generate.../README.md
Rawman Grey c8f8bbc5ac 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.
2026-10-25 11:19:54 +03:00

102 lines
6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.