Media preview (inline <video>/<audio>/pdf already exists): the UI auto-refreshes an expiring presigned media URL on playback error (re-presign via /api/fm/presign, keep currentTime). Claims: mediapreview (default true), previewurlttl (media files get a longer presigned TTL), maxpreviewmb...
php artisan boost:add-skill fluxfiles/laravel
Save this content to: .cursor/rules/media-delivery.md
**Media preview** (inline `<video>`/`<audio>`/pdf already exists): the UI auto-refreshes an expiring presigned media URL on playback error (re-presign via `/api/fm/presign`, keep `currentTime`). Claims: `media_preview` (default true), `preview_url_ttl` (media files get a longer presigned TTL), `max_preview_mb` (oversized → download placeholder). **Gated local media** (`FLUXFILES_LOCAL_PRIVATE=true`): local files serve through `/api/fm/stream?token=` (`StreamToken` + `RangeStreamer`, Range-capable, nginx `X-Accel-Redirect` fast path) instead of static URLs, so an `<video>` can't open without a token — the disk root must then not be served statically. `stream_token_ttl` claim.
**Posters for PDFs/videos** (`MediaPoster.php`, claim `media_posters`) are **opt-in (default `false`)** since core-v0.2.100 — they run Ghostscript/ffmpeg on every uploaded PDF/video. **Ghostscript older than 10.05.0 is treated as not installed** (published `-dSAFER` bypasses), for posters and PDF optimize alike. ffmpeg runs with a forced demuxer and `-protocol_whitelist file`.
**On-demand WebP/AVIF** (`GET /api/fm/img`, `ImageToken` + `ImageOptimizer::transform`): extends the upload-time variants with arbitrary sizes; cache lives in the file's `_variants/` (free invalidation via existing delete/trash cleanup). Width rounded 100px + clamped (`webp_max_width`), quality snaps to 60/75/80/90 → variant count bounded without per-request counting. **`format=auto` (default) content-negotiates AVIF→WebP→original from `Accept`** (AVIF needs intervention v3 + GD `imageavif`; `ImageOptimizer::avifSupported()` gate); avif/webp cache as separate keys (format is in `transformCacheKey`), response sends `Vary: Accept`; `format=avif`/`webp` force it. **Box sizing**: `height` + `fit` (cover crops / contain fits, default) + `dpr` (1/2/3, multiplies the requested size) — all folded into the cache key, never upsize. SVG/animated-GIF/bomb never converted. S3/R2 cache-hit 302-redirects to a presigned URL (no app egress). Image entries get `img_base` in `list()` when `webp_enabled` + a stream secret are set. Claims: `webp_enabled`/`webp_max_width`/`webp_default_quality`. **AVIF delivery is free/core.**
**BYOB disks:** `/stream` and `/img` have no access JWT, so their per-file tokens carry the disk config as an AES-GCM blob (`bd`, `FileManager::mediaTokenByob()`), never plaintext, with TTL capped at 1800 s; `MediaEndpoint::openTokenDisk()` reopens it through `CredentialEncryptor::openForDisk()`, the same checks the access JWT gets (SSRF, no shadowing a static disk). Accepted trade-off: the blob sits in URLs and access logs, and copies stay decryptable by anyone who later gets `FLUXFILES_SECRET`; so BYOB `/img` answers are never `public`-cacheable, and a key-auth disk's oversized blob skips `img_srcset`.
Both `/stream` and `/img` mint only when `FileManager::setStreamSecret()` is called — index.php always does, and the Laravel/WordPress **proxy** controllers now do too (unconditionally, in their shared `fileManager()` builder), so both routes are **proxied** by both adapters (ported directly into each controller; core's logic is now the framework-neutral `MediaEndpoint` class (`stream()`/`image()`, injectable rate limiter + X-Accel base), which the proxies can adopt). They reuse `FLUXFILES_SECRET` for their per-file tokens (`t=stream` / `t=img`, distinct types — one can't be used on the other's endpoint). SFTP-disk streaming rides the same `/api/fm/stream` route, so it's proxied too. The preview-time watermark **overlay** claim (`watermark_enabled`) is still NOT forwarded by either proxy adapter — that's a separate, still-valid restriction: the proxy's `/img` port doesn't implement overlay compositing, so forwarding the claim would mint a token whose watermark gets silently dropped (see `FluxFilesManager.php`'s comment on `watermark_enabled`).
Media preview (inline <video>/<audio>/pdf already exists): the UI auto-refreshes an expiring presigned media URL on playback error (re-presign via /api/fm/presign, keep currentTime). Claims: media_preview (default true), preview_url_ttl (media files get a longer presigned TTL), max_preview_mb (oversized → download placeholder). Gated local media (FLUXFILES_LOCAL_PRIVATE=true): local files serve through /api/fm/stream?token= (StreamToken + RangeStreamer, Range-capable, nginx X-Accel-Redirect fast path) instead of static URLs, so an <video> can't open without a token — the disk root must then not be served statically. stream_token_ttl claim.
Posters for PDFs/videos (MediaPoster.php, claim media_posters) are opt-in (default false) since core-v0.2.100 — they run Ghostscript/ffmpeg on every uploaded PDF/video. Ghostscript older than 10.05.0 is treated as not installed (published -dSAFER bypasses), for posters and PDF optimize alike. ffmpeg runs with a forced demuxer and -protocol_whitelist file.
On-demand WebP/AVIF (GET /api/fm/img, ImageToken + ImageOptimizer::transform): extends the upload-time variants with arbitrary sizes; cache lives in the file's _variants/ (free invalidation via existing delete/trash cleanup). Width rounded 100px + clamped (webp_max_width), quality snaps to 60/75/80/90 → variant count bounded without per-request counting. format=auto (default) content-negotiates AVIF→WebP→original from Accept (AVIF needs intervention v3 + GD imageavif; ImageOptimizer::avifSupported() gate); avif/webp cache as separate keys (format is in transformCacheKey), response sends Vary: Accept; format=avif/webp force it. Box sizing: height + fit (cover crops / contain fits, default) + dpr (1/2/3, multiplies the requested size) — all folded into the cache key, never upsize. SVG/animated-GIF/bomb never converted. S3/R2 cache-hit 302-redirects to a presigned URL (no app egress). Image entries get img_base in list() when webp_enabled + a stream secret are set. Claims: webp_enabled/webp_max_width/webp_default_quality. AVIF delivery is free/core.
BYOB disks: /stream and /img have no access JWT, so their per-file tokens carry the disk config as an AES-GCM blob (bd, FileManager::mediaTokenByob()), never plaintext, with TTL capped at 1800 s; MediaEndpoint::openTokenDisk() reopens it through CredentialEncryptor::openForDisk(), the same checks the access JWT gets (SSRF, no shadowing a static disk). Accepted trade-off: the blob sits in URLs and access logs, and copies stay decryptable by anyone who later gets FLUXFILES_SECRET; so BYOB /img answers are never public-cacheable, and a key-auth disk's oversized blob skips img_srcset.
Both /stream and /img mint only when FileManager::setStreamSecret() is called — index.php always does, and the Laravel/WordPress proxy controllers now do too (unconditionally, in their shared fileManager() builder), so both routes are proxied by both adapters (ported directly into each controller; core's logic is now the framework-neutral MediaEndpoint class (stream()/image(), injectable rate limiter + X-Accel base), which the proxies can adopt). They reuse FLUXFILES_SECRET for their per-file tokens (t=stream / t=img, distinct types — one can't be used on the other's endpoint). SFTP-disk streaming rides the same /api/fm/stream route, so it's proxied too. The preview-time watermark overlay claim (watermark_enabled) is still NOT forwarded by either proxy adapter — that's a separate, still-valid restriction: the proxy's /img port doesn't implement overlay compositing, so forwarding the claim would mint a token whose watermark gets silently dropped (see FluxFilesManager.php's comment on watermark_enabled).
How can I help you explore Laravel packages today?