feat(14.2-01): opt-in storage.uploads.serve route for public uploads

- surf mounts GET {storage.uploads.public_path_prefix}/{path...} on
  attach.StaticHandlerPublic when storage.uploads.serve is true
- bucket and database resolved per request; X-Content-Type-Options: nosniff
- surf README, storage and configuration docs describe the key
This commit is contained in:
2026-10-11 17:36:54 +02:00
parent 3408742fb0
commit 0b6d757b40
5 changed files with 83 additions and 1 deletions

View File

@@ -41,7 +41,19 @@ Application code gets the bucket with `app.Lookup[*blob.Bucket]()` and reads and
## Serving files
The framework does not mount a file route by itself. The application decides where files are served: mount `attach.StaticHandlerPublic` under `public_path_prefix` to serve originals and thumbnails and answer 404 for files whose row is not public, or put a web server or CDN in front of the bucket directory. Serve uploads from a separate origin when you can; the [Attachments](../database/attachments.md) page explains why.
The framework mounts no file route unless you ask for one. Set `storage.uploads.serve` to `true` (environment `SUMMER_STORAGE__UPLOADS__SERVE=true`, default `false`) and the router registers `GET <public_path_prefix>/{path...}` on `attach.StaticHandlerPublic`:
```yaml
storage:
uploads:
bucket_url: file://./storage/app/uploads/public
public_path_prefix: /storage/app/uploads/public
serve: true
```
The route serves originals and thumbnails and answers 404 for a missing file, an invalid path and a file whose `system_files` row is not public (`is_public` false). Every response carries `X-Content-Type-Options: nosniff`, so a browser does not sniff a stored file into HTML or script. The route looks up the bucket and the database on each request: `route:list` lists it without opening either, and it answers 404 until `serve` has published them. A plugin route with the same pattern fails boot as a duplicate.
Without the key, the application decides where files are served: mount `attach.StaticHandlerPublic` itself, or put a web server or CDN in front of the bucket directory. Either way, serve uploads from a separate origin when you can; the [Attachments](../database/attachments.md) page explains why. The built-in route is meant for development and for deployments that cannot split the origin yet.
## Upload size

View File

@@ -54,6 +54,7 @@ These are the keys most applications set. Each module's reference page lists all
| `http.body_limits.default_bytes`, `http.body_limits.upload_bytes` | Request body limits. Required by `serve` and `route:list`. | [surf](../../modules/surf/README.md) |
| `http.cors.*`, `http.trusted_proxies` | CORS and the proxies whose forwarded client IP is trusted. | [surf](../../modules/surf/README.md) |
| `storage.uploads.bucket_url` | Uploads bucket, `file://` or `mem://`. Required by `serve`. | [lagoon](../../modules/lagoon/README.md) |
| `storage.uploads.serve` | Mounts `attach.StaticHandlerPublic` under `storage.uploads.public_path_prefix` with `X-Content-Type-Options: nosniff`. Defaults to `false`. | [surf](../../modules/surf/README.md) |
| `admin.jwt.secret` | Secret for admin tokens. Required as soon as a plugin registers an admin controller. | [cabana](../../modules/cabana/README.md) |
| `backend.uri` | Path the admin is served under. Defaults to `/backend`. | [cabana](../../modules/cabana/README.md) |
| `mail.driver`, `mail.from`, `mail.smtp.*` | Mail delivery: `memory`, `log` or `smtp`. | [postcard](../../modules/postcard/README.md) |