Uploaders
File uploads are handled by Shrine, with Uppy on the front-end:
ImageUploader— base image uploaderRecordImageUploader— attach an image to any record:include RecordImageUploader::Attachment(:image)ModelImageUploader— model-level image uploader (used by content editor images)AudioUploader— sounds forf.sound_field(MP3, AAC/M4A, Ogg, WAV, WebM, FLAC):include AudioUploader::Attachment(:audio)on a record with anaudio_datajsonb column
Storage
Configured per environment in the generated Shrine initializer (config/initializers/shrine.rb):
| Environment | Storage |
|---|---|
| production | S3 (via the S3_* ENV variables) |
| development | local filesystem (public/uploads/) |
| test | in-memory |
Image processing uses libvips, blurhash placeholders are generated for progressive loading, and direct uploads go through the Shrine upload_endpoint: POST /admin/upload/image (20 MB max) and POST /admin/upload/audio (200 MB max), handled by the generated Admin::UploadsController. The endpoint only checks the size; each uploader validates the file type when it is attached to a record.
Images: types and sizes
ImageUploader accepts JPEG, PNG and WebP images, up to 20 MB and 8000 × 8000 pixels.
Image metadata
A photo carries metadata a published file shouldn't: its EXIF (the GPS location where it was taken, the camera's make, model and serial number, the date), XMP and IPTC. ImageUploader strips it as a file is uploaded, before it reaches the cache storage, so it's never stored nor published. libvips re-encodes the image in its own format (a JPEG or a WebP at quality 90, every frame of an animated WebP): it applies the EXIF orientation first, so a photo taken sideways stays upright, and keeps the color profile (ICC) with libvips 8.15 or later; an older libvips strips it too, and the image is shown as sRGB.
A file promoted from the cache to the store isn't processed again.
Images uploaded before Trek stripped this metadata still carry it (their sizes don't). To strip it, once the app's ImageUploader strips it on upload:
DRY_RUN=1 bin/rails trek:attachments:strip_image_metadata # only lists them
bin/rails trek:attachments:strip_image_metadataThe task goes through the files of the store storage (S3, the file system, or in memory): the JPEG, PNG and WebP images, and the sizes under derivatives/. It re-encodes each one carrying metadata as ImageUploader does on upload, and replaces it in place, under the same id and with the same Content-Type and Content-Disposition: every record, page version and content editor image block pointing to it keeps working, and so do the files no record points to anymore, which are still published. Each line gives the status (stripped, strippable on a dry run, failed when libvips can't read the file) and the file id. The records keep the file's former size in their metadata. Re-encoding can't be undone: enable the bucket's versioning, or copy it, first.
Sizes
Thumbnails and other sizes come from Shrine's derivation endpoint, mounted at /derivations/image: Trek::ThumbnailHelper.thumbnail_url(file, width, height), or file.derivation_url(:thumbnail, width, height), gives the URL of the image resized to fit within width × height. A size is made on its first request, then stored under derivatives/ and served from there. The sizes of an image are deleted with it, when Shrine deletes it: its record destroyed, or the image replaced or removed. Every size is a WebP at quality 80, without metadata, whatever the original's type.
The URLs are signed with the shrine.derivation_endpoint.secret_key of the credentials, which trek:install:attachments writes, or a key derived from secret_key_base without it.
Attachments and records reloaded mid-save
RecordImageUploader loads Trek::SavedAttachmentsReload on top of Shrine's activerecord plugin:
class RecordImageUploader < ImageUploader
plugin :activerecord
plugin Trek::SavedAttachmentsReload
endShrine resets every attacher when a record is reloaded, which is right for an explicit reload but loses a pending attachment when something reloads the record during the save — closure_tree does exactly that in its after_save callback when it reorders the siblings of a numerically ordered tree, as the generated Page is. The file would then never be promoted out of the cache storage. The plugin keeps, across such a reload, the attachers whose file was saved with the record; an attachment assigned but not saved is still discarded.
Files left in the cache storage
An uploaded file first lands in the temporary cache storage, and is promoted to the permanent store after the record is committed. A record whose promotion was skipped keeps pointing to the cache, and its file breaks as soon as the cache is cleaned up. To find those records, for every model and attachment, and promote the ones whose file still exists:
DRY_RUN=1 bin/rails trek:attachments:promote_cached # only lists them
bin/rails trek:attachments:promote_cachedEach line gives the status (promoted, promotable on a dry run, missing when the cached file is gone, changed when the record changed meanwhile), the record and attachment, and the file id. Missing files have to be uploaded again.