.env rather than a change in your code.
.env
STORAGE_BACKEND is unset, setting S3_BUCKET selects s3. Otherwise the app’s
STORAGE_BACKEND_FALLBACK applies, then local. See
automatic backends.
S3 and S3-compatible storage
Thes3 backend works with AWS S3 and with services that speak its API, such as MinIO,
Garage, Cloudflare R2 and Hetzner Object Storage. It needs the s3 extra:
.env
Buckets are treated as private.
upload() and get_url() return a presigned GET URL
that expires after S3_URL_EXPIRES seconds. If the bucket is public, or sits behind a CDN,
set S3_PUBLIC_URL and they return S3_PUBLIC_URL/<key> instead.
With S3_ENDPOINT set, Feather tells boto3 to send and check request checksums only when an
operation requires them. boto3 1.36 turned them on for every request, and several
S3-compatible services reject those requests.
Object keys are checked before any request is made: empty keys, a leading /, . and ..
segments, backslashes and control characters raise StorageError.
GCS credentials
- Inline JSON
- File path
- Default credentials
Recommended for deployment, since it fits on one line.
.env
Usage
In a route
Upload safety
LocalStorage serves files from static/, where the browser takes the content type from
the extension. Script-capable extensions are refused by default, so an uploaded page
cannot run on your origin. S3Storage applies the same lists, since a bucket served from
one of your domains has the same problem.
Setting the block list replaces it. The allow-list wins over the block list, which is how
you permit SVG when you need it.
An extension says nothing about content. When an upload has to be a particular type, check
its first bytes as well: a PDF starts with
%PDF-, and a renamed HTML file does not.
Private files, and showing a PDF in the page
A signed URL works for anyone who holds it until it expires. For files that belong to one person, stream them through a route that checks access on every request instead:<iframe> on your own page, allow same-origin framing. The
security headers default to frame-ancestors 'none', which
blocks your own iframe as well: