The upload pipeline proved bytes are safe; it didn’t prove anyone could use them. An editor was left typing UUIDs into a text box — technically correct, practically useless. This checkpoint finishes the arc: a real picker, real images on real pages, and the small-file problem solved inside the pipeline that already exists.
What I built
- The asset picker. The entry editor’s
assetfield now opens a library: every upload in the project, newest first, each showing its honest state — a real thumbnail served from the media origin when the upload is usable, “Checking…” while the worker verifies, “Rejected” with the reason when it isn’t. Choosing drops a chip into the field; chips reorder and remove within the list’s bound. Uploading in place goes through the samePOST /assetsroute, polls the job until the verifier settles, and auto-chooses a usable result. - Renditions at verify time.
verify_imagenow returns fixed downscales —w320andw1280— beside the stripped original, aspect-preserving, in the upload’s own format, and only when the source is actually wider. The worker writes each rendition topublic/{project}/{env}/{asset}/{name}before the row commits, and theassets.derivativescolumn records only the names that provably landed — so a partial failure retries rather than advertising an object that doesn’t exist. - Renditions on the origin.
GET /media/v1/…/{asset}/{name}serves a recorded derivative through the same store-read path as the original: immutable cache,nosniff, the storedContent-Type, ETag. The name is validated as lowercase-alphanumeric, at most sixteen characters, before it ever reaches an object key — soW320,../, and aw1280nothing wrote are all the same honest 404. - Images on real sites. The hero section gained optional
imageandaltfields; the shared renderer emitsassetUrl(id); the site build and the Studio’s preview canvas both wire that to the real/media/v1path. A published page’s image is the same existence-as-proof object the origin serves everywhere else.
Does it actually work?
- The spec drives it like an editor: upload a PNG in the picker → the worker settles it usable → it’s
auto-chosen → the draft saves → after a reload the chip shows a thumbnail fetched from the real media origin.
A fake image reports
rejected: not a supported imageand is never chosen. A list of two gets picked, reordered, trimmed to its bound, and the saved document keeps only the surviving id. - The rendition test is explicit: a 400×4 PNG serves
200 image/pngat both the original path and/w320, while/w1280(the source is narrower),/nope,/W320and a traversal attempt all answer 404 — and the asset row recordsderivatives = ["w320"]. - The load run mixes them: the media arm now reads the original and the
w320rendition alternately — 2,099 requests at 251/s, p99 16.2ms against a 700ms budget, zero failed. - 318 Rust tests, the testbed asserting
rendition w320: 200 image/pngthrough a real browser, and the site suite’s new image test all pass; the Studio and testbed both check clean.
What surprised me
- A feature shipped half-wired is a silent data loss. The asset field parsed, validated and saved — but the
form serializer had no
assetarm, so a list of uploads serialized to an empty string and silently never persisted. Nothing errored; the field just didn’t work. The picker spec caught it on the second assertion. resize_to_fillcrops. The first rendition attempt used it for “fit within width” and produced center-crops — the right-looking function with the wrong semantics. Aspect-preservingresizeis the honest one: a rendition is the same image, smaller.- Column-level grants forget new columns. Migration 0035 granted the worker UPDATE on
assets’ named columns; addingderivativesmeant the worker’s commit failed with permission denied until the new column got its own grant. Table-level grants would have covered it; the column-level list is deliberately tighter — it just has to grow by hand.