what

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:

what.toml
[uploads]
enabled = true
directory = "uploads"
max_size = "10mb"
allowed_types = ["image/*", "application/pdf"]
KeyDefaultDescription
enabledfalseEnable the upload endpoint
providerlocalStorage backend: local or r2
directoryuploadsDirectory for uploaded files (local only)
max_size10mbMaximum 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":

site/photos/new.html
<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:

FormatExampleMatches
Exact MIMEimage/pngOnly PNG images
Wildcard MIMEimage/*All image types (png, jpg, gif, webp, etc.)
Extension.pdfPDF 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.

Tip: An empty 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:

what.toml
[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.

Note: Rate limiting is applied to the upload endpoint by default (10 requests per 60 seconds). Configure this in the [rate_limit] section of what.toml.