Using Storage

Uploading

Every route and limit below is in the reference.

Three steps, and the bytes never touch your Worker — letting users upload files walks the browser side. Images and video want media instead, which wraps this.

1. Start it. Post the logical path and the size. You get back one presigned URL, or — past the multipart threshold — a URL per part.

2. Upload. The browser sends the bytes straight to your bucket.

3. Complete it. Post to the complete route, which confirms the bytes against the store rather than trusting the client’s word that it finished.

That third step is why an upload has a pending state at all. An object that never completes stays pending, holds its quota reservation, and is reclaimed by the daily sweep after its lifetime.

Big files resume

Past the threshold it is a multipart upload, and a dropped connection is recoverable:

GET /storage/:id/parts

Returns the parts already stored, plus a fresh URL for each missing one. So a resume is a partial re-upload rather than starting over — which at a few hundred megabytes is the difference between an annoyance and a lost afternoon.

POST /storage/:id/abort abandons one and drops its parts.

Downloads stream through the Worker, on purpose

An upload is presigned and a download is not, and that asymmetry is deliberate: a download is the moment authorization is decided, and a presigned URL is a URL anybody who has it can use.

So the bytes stream through, with range requests, entity tags and disposition handled for you.

GET /storage/:id/url mints a short-lived presigned direct URL where you genuinely want one — for a media player, say. It is minutes, not hours.

Uploaded bytes are treated as untrusted on the way out

This is the part worth reading even if you skim the rest.

A user’s file served from your origin is a script running on your origin, if a browser can be talked into executing it.

So every object response carries no-sniff and a locked-down content policy, and an active type — HTML, SVG, anything ending in an XML suffix, script — is served as an attachment whatever was stored.

Not blocked at upload, which is a different and worse trade: you would be refusing files people legitimately want to keep. Served in a way a browser will not execute.

Keys are opaque, paths are yours

You supply a logical path, and it is stored, indexed and listable — so GET /storage?prefix=… gives you a cursor-paginated listing.

The store key itself is server-derived and opaque. A client can never name an object or guess the one beside it.

Two properties fall out of that: enumeration is impossible, and renaming is a metadata change rather than a copy.

Shares

POST /storage/:id/shares

Mints a revocable link at its own short path, because the whole URL gets pasted into a chat window.

The token is the credential — that route is public by design. Revoking is a row you delete, and it takes effect immediately.

An expired share answers a specific code rather than a generic not-found, so a recipient can be told this link expired rather than this does not exist.

Quotas

Enforced per owner, in bytes, rather than per bucket.

An upload that would exceed it is refused at the start — before any bytes move — with a specific code.

Pending uploads hold their reservation, which is what stops somebody starting a hundred uploads to claim space they never use. The sweep reclaims abandoned ones.

Ownership

Objects belong to an authenticated owner, and every owner-scoped route reads the identity off the shared seam.

Without auth composed, those routes deny. That is a defined behavior rather than a crash, and it is also why storage is rarely composed alone.

The daily sweep

Reconciles in both directions: a row with no bytes behind it, and bytes with no row in front.

Both happen — an interrupted delete, a failed complete, an abandoned multipart — and neither is visible to anything else. A store nothing reconciles accumulates orphans forever, and the ones that cost you are the bytes.

Server-side copy

POST /storage/:id/copy

Duplicates without the bytes leaving the account. Useful for a template, a snapshot or a fork of somebody’s document.

The error codes worth handling

CodeStatusMeans
storage/not_found404No such object, or one you may not see
storage/forbidden403A public object you do not own
storage/quota_exceeded413The upload would put the owner over their limit
storage/upload_incomplete409Still pending — the upload never finished
storage/share_expired410The token is past its expiry

That first one is deliberate: an object you may not see answers not found rather than forbidden, because a 403 confirms the object exists.

ESC