frootbox / storage-s3
S3-compatible storage for the Frootbox CMS: media and files from forms in a bucket, public media delivered from there
Package info
github.com/frootbox-packages/storage-s3
Type:frootbox-extension
pkg:composer/frootbox/storage-s3
Requires
- php: >=8.5
- ext-simplexml: *
- frootbox/core: ^0.1
- nyholm/psr7: ^1.8
- psr/http-message: ^2.0
- symfony/http-client: ^8.1
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 22:33:49 UTC
README
S3-compatible storage for the Frootbox CMS: keep media and files from forms in a bucket instead of the web space, and let browsers load public media from there. Works with any service that speaks S3 and Signature Version 4, e.g. Hetzner Object Storage, IONOS, Cloudflare R2, Wasabi, Backblaze B2, AWS or a self-hosted gateway.
Set up
Create two buckets: one private for data, one public for what browsers
load. Never use one bucket for both: originals, drafts and files from
forms must not be public. Then set in the .env:
STORAGE_DSN="s3://<key>:<secret>@fsn1.your-objectstorage.com/site-data?region=fsn1"
PUBLIC_STORAGE_DSN="s3://<key>:<secret>@fsn1.your-objectstorage.com/site-media?region=fsn1&url=https://media.example.org"
Key and secret are URL-encoded (/ is %2F, + is %2B). Options:
| Option | Meaning |
|---|---|
region |
The region of the service, default us-east-1 (R2: auto) |
url |
Public store only: the address for browsers, best your own subdomain or a CDN; default the address of the bucket |
path_style=1 |
The bucket in the path instead of the host, e.g. for MinIO or a gateway |
prefix |
Before every key, for several websites in one bucket |
acl |
Public store only, e.g. public-read for a service without public buckets |
scheme=http |
Only for a test server on your own machine |
Each variable works alone: only STORAGE_DSN moves the originals and
keeps the generated sizes in public/media/.
Move the files there and check both stores:
bin/frootbox storage:push --dry-run
bin/frootbox storage:push
bin/frootbox storage:check
storage:push copies storage/media/ and storage/private/forms/,
only what is missing. Sizes of images are generated again in the public
bucket. Once the website works, storage/media/, storage/private/forms/
and public/media/ (but not public/media/fonts/) can go.
How it works
- Addresses stay
/media/<id>/<version>/…. The first request generates a size, stores it in the public bucket withCache-Control: public, max-age=31536000, immutableand marks it belowvar/storage/marks/. Pages then get the address in the bucket; an old address in a cached page answers with 302 to the bucket. - Videos and documents are copied into the public bucket on their first request, within the service if both buckets share it; the bucket answers Range itself.
- Drafts, media that are not public yet and files from forms come only through PHP, after the check of permissions.
- Images, SVG and videos need a file on disk now and then. Copies lie in
var/storage/and go after a day without use. - Uploads in parts, fonts, backups and
var/stay on the web space.
Good to know
- Backup: files in a bucket are not part of a backup of the web
space. Turn on versioning at the service, or copy them back now and
then with
bin/frootbox storage:pull. - Privacy: visitors load public media from the server of the service, which sees their IP address. The service processes data for you like your host: you need a data processing agreement, best with a service in the EU.
- Normal web space: only outgoing HTTPS is needed, with or without cURL.
MIT licensed.
Source and issues
This package is developed in the Frootbox monorepo and mirrored here read-only. Questions, bugs and ideas go there, see the documentation.