a plugin can claim a file type the platform cannot read

the file browser's thumbnailer read what sharp and ffmpeg read, and that list was a
constant in core. HEIC forced the question: sharp's bundled libheif has no HEVC
decoder, and teaching core to shell out to ImageMagick means the PLATFORM acquiring
a system dependency for one format. that is a plugin's job.

so a manifest may declare `fileTypes` — extensions, plus a thumbnail path and a
render path on its own sidecar. `ls` unions those with core's, so a file only
promises a thumbnail when something can actually produce one. `/thumb` forwards a
claimed type to whoever claimed it.

CORE DOES NOT KNOW av-club EXISTS. it reads a claim at runtime from whatever is
installed and enabled, calls a path by name, and falls back to a file icon when
nothing claims the type. no import, no name, no special case.

PROVEN BY DISABLING IT: heic reports thumb=image with av-club enabled, thumb=none
disabled, image again re-enabled. that is the whole requirement — the file browser
is what it was without the plugin — and it is a test rather than a claim.

the plugin's bytes are cached by CORE, under the same key everything else uses, so
a HEIC thumbnail is produced once and then served from disk like any other. that is
why `buildThumbnail` takes a producer rather than there being a second path beside
it: the cache key with its mtime and size, the concurrency cap and the remembered
failures all live in that one function, and a parallel path would be a second place
to forget the negative caching.

TWO THINGS FOUND BY WIRING IT UP. a claimed-but-not-running sidecar is skipped
rather than recorded, or `ls` promises a thumbnail that cannot be fetched while a
plugin restarts. and the sidecar authenticates: it accepts `x-officer-user` from
anything on loopback, by its own design, so core sends the account the request is
FOR — passing anything else would be lying to a plugin that scopes per user.

verified: HEIC 2 MB in, 12 KB out, 5.4s cold and 101ms cached; png and mp4
unchanged; 1665 tests; docs:check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-29 07:50:51 +01:00
co-authored by Claude Opus 5
parent 9cb875ca79
commit 111618bd7f
4 changed files with 199 additions and 6 deletions
+32
View File
@@ -477,6 +477,38 @@ export type PluginManifest = {
settings?: PluginSetting[];
websocket?: { sidecar: string };
/**
* File types this plugin can show that the platform cannot.
*
* ── Why this exists ──
*
* The file browser's thumbnailer reads what sharp and ffmpeg read, and that list is a constant in
* core. HEIC is the case that forced the question: sharp's bundled libheif has no HEVC decoder, and
* teaching core to shell out to ImageMagick would mean the platform acquiring a system dependency for
* one format — which is a plugin's job, not core's.
*
* So a plugin may CLAIM extensions. `ls` unions these with core's own, so a file only promises a
* thumbnail when something can actually produce one; `/thumb` forwards a claimed type to the plugin
* that claimed it; and the viewer asks for `render` when opening one full size.
*
* ── The dependency still points the right way ──
*
* Core does not import plugin code and does not know av-club exists. It reads a claim at runtime from
* whatever is installed and enabled, calls a route by name, and falls back to a file icon when nothing
* claims the type. Uninstall the plugin and the file browser is exactly what it was.
*
* `thumbnail` and `render` are paths on the plugin's own sidecar, called with `?path=<absolute>` —
* plus `&size=` for a thumbnail and `&width=` for a render. Both must answer image bytes.
*/
fileTypes?: {
/** Lowercase, no leading dot. `['heic', 'heif']`. */
extensions: string[];
/** Sidecar path returning a small image. Omit and the platform will not offer thumbnails for these. */
thumbnail?: string;
/** Sidecar path returning a full-size displayable image. Omit and the viewer will not offer them. */
render?: string;
};
osDependencies?: OsDependency[];
/**