Files
Upload, manage, and serve files using Cloudflare R2 with presigned URLs and auto-thumbnails.
#File storage
EmuView stores files on Cloudflare R2 using a secure three-step presigned URL flow. Files upload directly from the browser to R2 — they never pass through the Worker, keeping your API fast and your costs low.
#What you can do
| Action | How |
|---|---|
| Upload a file | Request an upload token, PUT to the presigned URL, confirm the upload |
| Download a file | Request a presigned download URL (5-minute expiry) |
| Auto-thumbnails | JPEG and PNG uploads get thumbnails generated automatically (per-container off switch) |
| Associate with records | Store the file ID in a file field on any collection |
| Manage quota | 5 GB per project by default (configurable) |
#Key concepts
| Concept | What it means |
|---|---|
| Presigned URL | A time-limited URL that grants direct access to R2 without going through EmuView |
| Upload token | A response from POST /api/v1/files/upload-token containing the presigned upload URL and file ID |
| File field | A collection field of type file that stores a reference to an R2 object |
| Size variant | A resized version of an image (e.g., thumbnail) generated on upload confirmation |
#Server-side variants
Variants are generated on the server when an upload is confirmed. Both halves of that are configurable, and each resolves container override → global setting → built-in default:
| What | Container setting | Global setting | Default |
|---|---|---|---|
| Whether it runs | Server-side variants | files.variants_enabled |
on |
| What it emits | Variant format | files.variant_format |
auto (webp, png/jpeg fallback) |
| Which sizes | Thumbnail sizes | files.image_sizes |
{ "thumbnail": 320, "large": 1024 } |
Turn Server-side variants off for a container whose derivatives are produced by another flow — otherwise the built-in pass writes variant objects beside every original that nothing will read. Leaving Thumbnail sizes blank does not do this: blank means inherit, and that chain ends at the built-in sizes.
Pinning a format writes that format or nothing, so no second format lands at a
neighbouring key. auto keeps the historical behaviour: webp, falling back to
png/jpeg only if the webp encoder cannot start.
Both apply to new uploads; use Regenerate on the container for existing files.
#HEIC photos are converted before they are uploaded
The server-side resizer has no HEIC decoder, so a HEIC stored as a HEIC would
get no variants and nothing downstream could thumbnail it. The browser decodes
HEIC wherever the platform does, so the upload component converts it to WebP on
the device — before the PUT — and uploads that instead, with the filename and
declared MIME rewritten to match (IMG_0421.HEIC becomes IMG_0421.webp).
The HEIC original is discarded, and with it any depth map, HDR gain map and 10-bit range the file carried. Only the WebP is stored; there is no copy of the original to recover. This is deliberate — the alternative is an original in the bucket that nothing can read.
A browser that can decode neither HEIC nor encode WebP refuses the upload with a message asking for a JPEG export, rather than storing an original that cannot be thumbnailed.
#Upload flow
1. Your app → POST /api/v1/files/upload-token (get presigned URL + file ID)
2. Your app → PUT {presignedUrl} (upload directly to R2)
3. Your app → POST /api/v1/files/confirm-upload/{id} (mark complete, trigger thumbnails)
4. Your app → Store fileId in a collection record's file field
Downloads follow the same presigned pattern — request a token, then fetch directly from R2:
sequenceDiagram
participant App as Your app
participant GW as Gateway Worker
participant R2 as Cloudflare R2
App->>GW: POST /api/v1/files/upload-token
GW-->>App: Presigned PUT URL + file ID
App->>R2: PUT file (direct upload, 120s window)
App->>GW: POST /api/v1/files/confirm-upload/:id
GW->>R2: Verify upload, generate thumbnails
App->>GW: GET /api/v1/files/download-token/:id
GW-->>App: Presigned GET URL (300s)
App->>R2: GET fileThe SDK wraps these three steps into a single sdk.files.upload() call — see SDK file methods. Endpoint parameters and error codes are in the files API reference.
#Storage limits
| Limit | Value |
|---|---|
| Default quota per project | 5 GB |
| Max file size | 100 MB per file (Cloudflare Workers limit) |
| Upload URL expiry | 120 seconds |
| Download URL expiry | 300 seconds |
#Guides in this section
- Upload flow — Step-by-step guide to uploading files from the browser
- Download and serve — Get presigned download URLs and serve files
- Thumbnails — Configure automatic image resizing and size variants