- TestUploadsMount: key off registers nothing; on, a public blob is served with its content type and nosniff, HEAD has no body, private, missing, partition and encoded traversal paths 404, an unpublished bucket or database 404s, a custom prefix is honoured - mountUploads refuses a plugin route at or under the prefix, so BuildRouter fails instead of only an identical pattern failing at Assemble (README and storage docs updated) - TestAccessLog: pattern and status for 200, handler 404 and recovered panic, path only for unmatched requests, locale-prefixed requests keep the pattern, integer duration_ms, no query, header or Authorization value, ResponseController flush still works
4.1 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Storage | Configure the uploads bucket that serve opens, choose file or memory bucket URLs, serve stored files, and size upload routes. | services | 90 |
Storage
WinterCMS stores uploads on a Laravel filesystem disk. SummerCMS stores them in one gocloud.dev bucket, opened from storage.uploads.bucket_url by the attach package of lagoon. The serve command opens the bucket at start-up, before it accepts requests, and publishes it on the application; an empty bucket_url stops the start-up.
Bucket URLs
| URL | Stores |
|---|---|
file:///var/lib/acme/uploads |
In a directory on the server. |
mem:// |
In memory, for tests. Everything is lost when the process exits. |
The keys go in config/storage.yaml:
uploads:
bucket_url: file:///var/lib/acme/uploads
public_path_prefix: /storage/uploads
Files are laid out as WinterCMS lays out its uploads disk, so a copy of a WinterCMS storage/app/uploads/public directory can serve as the bucket after a port. public_path_prefix is the URL prefix that file and thumbnail URLs start with. attach.PublicURL builds the URL of any blob key from it, and attach.File.URL the URL of an original.
The WinterCMS layout
A port whose clients already store or compare upload URLs keeps WinterCMS's URLs too. WinterCMS serves public uploads from storage/app/uploads/public under the URL path /storage/app/uploads/public (cms.storage.uploads.path plus /public). Root the bucket at that directory and use the same prefix:
uploads:
bucket_url: file://./storage/app/uploads/public
public_path_prefix: /storage/app/uploads/public
An original then has the URL /storage/app/uploads/public/<partition>/<disk_name>, as WinterCMS's File::getPath() returns, and a 200 by 200 cropped thumbnail /storage/app/uploads/public/<partition>/thumb_<id>_200_200_0_0_crop.<ext>, as getThumb() returns. The framework default stays /storage/uploads, so only an application that sets these keys changes its URLs.
Application code gets the bucket with app.Lookup[*blob.Bucket]() and reads and writes it through the gocloud.dev/blob API. Model attachments, thumbnails and deleting files after commit are covered in Attachments.
Serving files
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:
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. The route owns the whole prefix, so start-up fails when a plugin registers a route at or under it.
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 page explains why. The built-in route is meant for development and for deployments that cannot split the origin yet.
Upload size
Every non-raw route has a request body limit of http.body_limits.default_bytes. A route that accepts uploads raises its own limit with the body.limit:<bytes> middleware; see Routing. http.body_limits.upload_bytes is required and validated at start-up, but the framework applies it to no route; a plugin that wants its upload routes to follow it reads it in Register and puts the value in the route's body.limit.