feat(12.2-02): add file removal, caption, reorder and protected downloads
- DELETE, PUT and POST reorder under .../{id}/files/{field}, each scoped by one parent query (404 for a foreign file)
- protected download and thumb routes: is_public=false only, nosniff, private no-store, sandbox CSP, inline only for jpeg/png/gif/webp
- the save applies deferred removals, replaces attachOne files and rechecks maxFiles and required
- blobs of deleted files are removed after commit
- swagger2openapi emits binary content for file responses
- admin OpenAPI, TS types, conformance, README and attachments docs
This commit is contained in:
@@ -136,6 +136,8 @@ The field takes the generic keys plus these WinterCMS keys:
|
||||
|
||||
Uploads are deferred until the form is saved, on the create form and the update form alike, as WinterCMS's file upload widget does. The admin SPA makes a random session key when it opens a form and sends it in the `X-Session-Key` header with every upload and with the save. The upload stores the file unattached and records a pending binding for that key and the signed-in administrator; the record's create or update save attaches every pending file of the form's fileupload fields inside its own transaction. If the save fails with a 422, the uploads stay pending for the next attempt; if the form is left without saving, the daily `deferred:purge` removes them. A session key is only ever seen by the administrator who used it.
|
||||
|
||||
Removing a file is deferred the same way: the file disappears from the form at once and is deleted by the save (a pending upload that is removed is deleted at once). On an attachOne relation, saving a new upload deletes the file it replaces. Captions (with `useCaption`) and the order of an attachMany field are saved at once, as in WinterCMS. After applying the session's work, the save checks `maxFiles` and, for a `required: true` fileupload field, that at least one file is attached; a failure is a 422 on the field and keeps the pending work. Files of a protected relation are shown through authenticated admin routes only; see [Protected files in the admin](../database/attachments.md#protected-files-in-the-admin).
|
||||
|
||||
The limits are enforced on the server: the upload route caps the request body at the smaller of `http.body_limits.upload_bytes` and `maxFilesize` plus 64 KiB (413 `payload_too_large` past it), and a file that is too large, of a type the field does not allow, or not a valid image in image mode is a 422 on the field.
|
||||
|
||||
## What a save may write
|
||||
|
||||
@@ -105,6 +105,15 @@ A model declares its attachment relations, the Go form of WinterCMS's `$attachOn
|
||||
|
||||
Public and protected files live in the same bucket. A protected file (`is_public` false) is kept private by three rules: its disk name is unguessable, the framework never builds a public URL for it, and the host application mounts `attach.StaticHandlerPublic`, which answers 404 for it, or serves the bucket with directory listing turned off. `attach.File.ThumbKey` returns a thumbnail's blob key instead of its URL, so an authenticated route can stream a protected thumbnail itself.
|
||||
|
||||
### Protected files in the admin
|
||||
|
||||
A `type: fileupload` field on a protected relation never shows a public URL in the admin; see [Forms](../backend/forms.md#file-uploads). The admin SPA reads such a file through two authenticated admin API routes under the `backend` guard:
|
||||
|
||||
- GET `{prefix}/api/v1/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}/download` streams the original.
|
||||
- GET `{prefix}/api/v1/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}/thumb` streams the preview thumbnail, built with `attach.File.ThumbKey`; a file that is not a JPEG, PNG, GIF or WebP image has none.
|
||||
|
||||
Both routes look the file up with one query scoped to its record, which the controller's `pact.FormExtendQuery` must let the administrator load, or to an upload pending in the administrator's own form session (`X-Session-Key`). A file of another record, a public file and a file outside that scope all answer 404. Every response carries `X-Content-Type-Options: nosniff`, `Cache-Control: private, no-store` and `Content-Security-Policy: default-src 'none'; sandbox`. Only JPEG, PNG, GIF and WebP are served inline with their own type; any other file, an SVG included, is sent as an `application/octet-stream` attachment, so a stored file never runs script in the admin's origin.
|
||||
|
||||
## Deleting files after commit
|
||||
|
||||
A rolled-back transaction can restore a row but not the bytes of a deleted blob. Deleting an owner's files is therefore split in two:
|
||||
|
||||
Reference in New Issue
Block a user