put for bytes you have, copy and move to rearrange them, updateJson for a read-modify-write, and signedUploadUrl to hand one write to somebody else.
lib/reports.ts
bucket is the client from Connecting. Everything here runs on your server with the bucket token.
For files a user picks in the browser, do not proxy the bytes through your app. Use an upload handler instead.
put
Options
Every option is optional.Size is a decimal byte count like 4096 or '20mb' (Types). The content type grammar and wildcards are on Constraints.
Return value
CompletedBlob (Types). On a private bucket url and versionedUrl are undefined.
Bodies
app/api/avatar/route.ts
put accepts these body types. If the body does not carry its own length or type, declare size or contentType yourself.
The default content type is
application/octet-stream. An explicit contentType wins over what the body carries.
A Request with no body, or one that has already been read, throws empty_body. Anything else is a TypeError.
Streams and unknown lengths
ReadableStream has none. Pass one of the two:
size fails the request rather than being stored at the wrong length. A Request that arrived chunked has no content-length and counts as an unknown length too.
When proxying bytes through a route, keep maxSize under the platform’s own request body cap, since that refusal happens before your route runs. See Platform body limits.
Paths
/ as structure. It is percent-encoded for you, so spaces and unicode are fine. . and .. segments are rejected, not normalized, by every method that takes a path.
uniquePath
uniquePath builds a safe path out of values you do not control, like a filename from a browser. Slashes in the template literal are structure. Every ${} value is reduced to a single slugged filename, so it can never add a directory, and one random suffix goes on the finished path:
- Lowercased. Runs of anything that is not a letter or a number become
-. - Letters and digits from any script survive, so
café.pdfkeepscafé. - The stem is capped at 64 characters, the extension at 8.
photo.png never land on the same object. To overwrite on purpose, write the path yourself.
Metadata
metadata is a flat Record<string, string> stored as x-amz-meta-* headers. Three rules:
- Keys come back lowercased. Write them lowercase to begin with.
- Values must be printable ASCII. Anything else is refused with
invalid_input. Percent-encode other text and decode it on the way back. - It comes back from
info()andget(), notlist(). Reading metadata for many objects is oneinfo()call each.
Conditional writes
multipart: true throws:
updateJson
updateJson runs in the SDK, not in storage. It reads the document, calls your function with the parsed value, and writes the result back with ifUnchanged. If somebody wrote in between, it pauses briefly, reads again and re-runs your function. After maxAttempts failed writes it throws conflict.
- Your function gets
nullwhen there is nothing to read. An empty object reads asnulltoo. - It may be async. It runs on every attempt, so keep it a pure transform.
- The object is written as
application/json. Existing metadata is carried over unless you pass your own.
Options
Every option is optional.copy and move
copy runs inside storage, so the bytes never travel through your app. Storage has no rename, so move is a copy followed by a delete of the source. Both return the destination’s record. A missing source throws not_found.
An existing destination is overwritten. There is no allowOverwrite here because storage does not honor a precondition on a copy’s destination.
Options
Every option is optional, andmove takes the same ones as copy.
With no options the destination is an exact copy. Passing any one of them makes storage rewrite all three, so the SDK reads the other two off the source first and sends them back unchanged.
A move is not atomic. If the copy lands and the delete fails,
move throws move_left_a_copy and keeps both objects. Retry the source delete to recover; see Deleting.
Large bodies
multipart changes the threshold: a size sets a new one, true always uses parts, false never does.
- A single PUT cannot carry more than about 5 GiB.
multipart: falseon a body that big throwstoo_large. - Parts are sent one at a time. Any failure aborts the whole upload before throwing, so nothing is left behind.
- A body that does not match a declared
sizethrowsinvalid_input.
Signed upload URLs
signedUploadUrl returns { url, headers, expiresAt }: a URL somebody else can PUT exactly one object to. Use it for a CLI, a build step, or a server-to-server job whose bytes you do not want to relay.
Every option is optional.
headers are signed into the URL and must be sent verbatim. Drop one, change one, or add one, and storage answers 403. That is what stops the uploader from changing metadata.
expiresAt may be sooner than what you asked for. Cache the link until then rather than computing your own deadline; see Use expiresAt.
For a browser upload, use the upload handler instead. A signed URL is one PUT: no multipart, no resume, and nothing tells your server it happened.
Next steps
Reading
get, info, exists and paging through list.Deleting
One path, a list, a prefix, and what a partial delete reports.
Caching
What
cache accepts and why it is written once, at upload.Connecting
Client options, Cloudflare Workers, and using an S3 client directly.