fix(12-05): store WinterCMS's broken-image thumbnail for an unusable original (T-12-16)

A photo whose original is missing, does not decode or declares more than
4096x4096 pixels made attach.File.Thumb return an error, and every listing
that shows the photo answered 500 from then on: one 100-byte PNG uploaded
by any household member broke GET collections and the album for everyone.

Thumb now follows WinterCMS's File::makeThumb catch branch: it logs the
reason at warn level, stores WinterCMS's BrokenImage picture (exported as
attach.BrokenImagePNG) under the thumbnail key and returns its URL. Invalid
arguments, storage errors and encode failures are still errors.
This commit is contained in:
Jakub Zych
2026-10-02 15:15:26 +02:00
parent 2f71aeb534
commit 1307060e15
4 changed files with 138 additions and 6 deletions

View File

@@ -84,7 +84,7 @@ fmt.Println(url)
URLs start with `storage.uploads.public_path_prefix` (`/storage/uploads` by default). `attach.File.URL` returns the URL of the original, the path WinterCMS's `File::getPath()` returns, and `attach.PublicURL` the URL of any blob key; [Storage](../services/storage.md#the-wintercms-layout) shows the configuration that reproduces WinterCMS's URLs exactly.
Originals in JPEG, PNG, GIF and WebP can be thumbnailed. The thumbnailer cannot write WebP, so the thumbnail of a `.webp` original holds JPEG bytes under the original's `.webp` name, and it is stored with the `image/jpeg` content type. Before decoding, the thumbnailer reads the image size from the file header and refuses an image larger than 4096 by 4096 pixels. `attach.StaticHandler` serves originals and thumbnails under that prefix; `attach.StaticHandlerPublic` does the same and answers 404 for a row whose `is_public` flag is false. Mount the gated handler when a bucket holds any private file.
Originals in JPEG, PNG, GIF and WebP can be thumbnailed. The thumbnailer cannot write WebP, so the thumbnail of a `.webp` original holds JPEG bytes under the original's `.webp` name, and it is stored with the `image/jpeg` content type. Before decoding, the thumbnailer reads the image size from the file header, so an image larger than 4096 by 4096 pixels is never decoded. As WinterCMS's `File::makeThumb` does, an original that is missing, does not decode or is too large gets WinterCMS's 200 by 200 broken-image picture (`attach.BrokenImagePNG`) stored as its thumbnail, and the reason is logged at warn level: one unusable upload never makes the pages that list it fail. `attach.StaticHandler` serves originals and thumbnails under that prefix; `attach.StaticHandlerPublic` does the same and answers 404 for a row whose `is_public` flag is false. Mount the gated handler when a bucket holds any private file.
> [!WARNING]
> Serve uploads from a separate origin, or at least never mount the ungated handler on the application's own origin. An uploaded file served with its own content type from the API's origin can run script in that origin.