File Uploads
Accept file uploads from forms. Store locally or on Cloudflare R2. Files are validated, renamed with UUIDs, and served statically.
Upload Configuration
Enable uploads in what.toml:
[uploads]
enabled = true
directory = "uploads"
max_size = "10mb"
allowed_types = ["image/*", "application/pdf"]
| Key | Default | Description |
|---|---|---|
enabled | false | Enable the upload endpoint |
provider | local | Storage backend: local or r2 |
directory | uploads | Directory for uploaded files (local only) |
max_size | 10mb | Maximum file size (kb, mb, gb) |
allowed_types | [] (all) | Allowed MIME types and extensions |
Upload Form
Upload files by posting to /w-upload/:collection with enctype="multipart/form-data":
<form action="/w-upload/photos" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="#_csrf#">
<label>Title</label>
<input type="text" name="title" required>
<label>Photo</label>
<input type="file" name="file" accept="image/*" required>
<button type="submit" class="btn btn-primary">Upload</button>
</form>
The uploaded file is saved with a UUID filename (e.g., a3f2b1c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c.jpg) to prevent name collisions and path traversal. The original filename is stored in the collection record.
File Serving
Uploaded files are served at /uploads/filename:
<what>
fetch.photos = "local:photos"
</what>
<loop data="#photos#" as="photo">
<div class="card">
<img src="#photo.file_url#" alt="#photo.title#">
<p>#photo.title#</p>
</div>
</loop>
When using local storage, the file_url field contains a path like /uploads/a3f2b1c4.jpg. When using R2, it contains the full public URL.
Allowed Types
The allowed_types array supports three formats:
| Format | Example | Matches |
|---|---|---|
| Exact MIME | image/png | Only PNG images |
| Wildcard MIME | image/* | All image types (png, jpg, gif, webp, etc.) |
| Extension | .pdf | PDF files (application/pdf) |
[uploads]
enabled = true
# Allow images and common document types
allowed_types = [
"image/*",
".pdf",
".doc",
".docx",
".csv",
".txt",
".zip",
]
Supported extension mappings: .pdf, .doc, .docx, .xls, .xlsx, .csv, .txt, .zip.
allowed_types array means all file types are accepted. Always specify types in production to prevent abuse.
Cloudflare R2
Store uploads in Cloudflare R2 for scalable, globally distributed file serving:
[uploads]
enabled = true
provider = "r2"
max_size = "25mb"
allowed_types = ["image/*"]
[cloudflare]
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
r2_bucket = "my-uploads"
r2_public_url = "https://pub-abc123.r2.dev"
Files are uploaded to the R2 bucket via the Cloudflare API. The r2_public_url is used to construct the public URL stored in the collection record.
File Cleanup
Uploaded files are automatically deleted from storage when:
- The associated record is deleted (via a
w-action="delete"form) - A validation failure prevents the record from being saved
This applies to both local storage and R2. Orphaned files from failed uploads are cleaned up immediately.
[rate_limit] section of what.toml.