Skip to content

Latest commit

 

History

History
184 lines (139 loc) · 6.63 KB

File metadata and controls

184 lines (139 loc) · 6.63 KB

R2 — Object Storage

What it is

Namespace-level object storage, accessed from a worker through env.<BUCKET> with head / get / put / delete / list. Use it for large blobs (images, PDFs, archives), or for anything larger than — and less suited to — KV.

bucket_name is a platform-local virtual bucket name, not a Cloudflare account-level bucket. Deleting a worker does not delete R2 data.

When to use

  • Files (images, attachments, PDFs).
  • Anything where a single blob is larger than a few KB.
  • Content served for download via CDN — but for static/build artifacts, check assets.md first.

Prefer KV for small key-value lookups — see kv.md. Use D1 for relational data — see d1.md.

Wrangler configuration

[[r2_buckets]]
binding = "IMAGES"
bucket_name = "inspection-images"

Or in wrangler.jsonc:

{
  "r2_buckets": [
    { "binding": "IMAGES", "bucket_name": "inspection-images" }
  ]
}

R2 buckets are lazy — no pre-creation needed. The binding works on first use; the bucket appears once the worker writes its first object.

Wrangler's local_dev.experimental_s3_credentials setting exposes an R2 bucket through a local S3-compatible endpoint. WDL deploy does not run that local service and rejects the field instead of silently dropping the credentials.

Custom metadata keys are normalized to lowercase on read, following HTTP header semantics. head() exposes HTTP metadata and custom metadata, so for authorization it is treated like reading the object body.

Worker-side reads and writes

export default {
  async fetch(request, env, ctx) {
    // PUT
    await env.IMAGES.put("receipts/2026-04-29.pdf", request.body, {
      httpMetadata: { contentType: "application/pdf" },
    });

    // GET — returns null when not found
    const obj = await env.IMAGES.get("receipts/2026-04-29.pdf");
    if (!obj) return new Response("not found", { status: 404 });
    return new Response(obj.body, {
      headers: { "Content-Type": obj.httpMetadata?.contentType ?? "application/octet-stream" },
    });

    // RANGE GET
    const preview = await env.IMAGES.get("receipts/2026-04-29.pdf", {
      range: { offset: 0, length: 1024 },
    });

    // Conditional GET / HEAD
    const fresh = await env.IMAGES.get("receipts/2026-04-29.pdf", {
      onlyIf: { etagDoesNotMatch: obj.etag },
    });

    // HEAD — metadata only, no body download
    const meta = await env.IMAGES.head("receipts/2026-04-29.pdf");

    // LIST — paginated; including metadata reads each object's HEAD
    const page = await env.IMAGES.list({ prefix: "receipts/", limit: 100 });
    const pageWithMeta = await env.IMAGES.list({
      prefix: "receipts/",
      include: ["httpMetadata", "customMetadata"],
    });

    // DELETE
    await env.IMAGES.delete("receipts/2026-04-29.pdf");
  },
};

Reads and metadata

get() supports the common range GET shapes (offset / length / suffix / raw header) and conditional reads. onlyIf on get() / head() supports ETag conditions and upload-time conditions; put(..., { onlyIf }) currently supports only ETag conditions and returns null when the precondition fails.

list({ include: ["httpMetadata", "customMetadata"] }) issues extra HEAD requests to hydrate metadata for the listed objects, under a concurrency cap. For large scans, do not enable the metadata include by default; use it only when the list results genuinely need metadata for display or decisions.

When httpMetadata is supplied as a Headers object, an Expires header must use canonical IMF-fixdate syntax, such as Wed, 21 Oct 2015 07:28:00 GMT. Malformed values are rejected before the host binding call.

put size cap

put(stream, ...) buffers the stream first, then sends a single S3 PUT, with a 25 MiB cap. The platform does not support multipart upload yet; exceeding the cap fails instead of falling back to chunked upload.

Inspecting from the CLI

wdl r2 buckets list

wdl r2 objects list <bucket> [--prefix <p>] [--delimiter <d>] [--limit <n>] [--cursor <c>]
wdl r2 objects head <bucket> <key>          # metadata only, no body download
wdl r2 objects get  <bucket> <key> --out file  # download
wdl r2 objects delete <bucket> <key> --yes  # destructive — confirm first

wdl r2 objects get writes raw object bytes. Pipe or redirect stdout when you intend to stream bytes; on an interactive terminal, pass --out <path>. --out accepts an explicit path outside the project and currently uses normal overwrite semantics: an existing file is replaced, and a symlink target is followed. Verify the destination before downloading.

When the list is truncated, the output includes Next cursor: <c>; pass it to the next --cursor to keep paging (wdl r2 buckets list also supports --cursor / --limit, with --limit in 1..1000):

wdl r2 objects list receipts --limit 100
# ...
# Next cursor: eyJrZXkiOiJyZWNlaXB0cy8wMDk5In0
wdl r2 objects list receipts --limit 100 --cursor eyJrZXkiOiJyZWNlaXB0cy8wMDk5In0

wdl r2 objects head is a read-only inspection tool — confirm the target object is correct before any destructive operation. For a missing object, control follows HTTP HEAD semantics and returns an empty 404; the CLI shows the status code, with no JSON error body to parse.

R2 object keys may contain leading, trailing, or doubled / path separators. The CLI preserves those empty path segments. It rejects . and .. segments so an object key cannot be confused with a control-plane URL path traversal.

Cleanup after worker deletion

Deleting a worker does not delete R2 data. R2 objects remain after wdl delete worker <name>. To clean up:

wdl r2 objects list <bucket>            # confirm what exists first
wdl r2 objects delete <bucket> <key>    # delete one at a time, each prompts

Confirm with the user before adding --yes. Object deletion is irreversible.

Anti-patterns

  • ❌ Using R2 for small key-value data (single-string flags and the like). KV is cheaper for both reads and writes — see kv.md.
  • ❌ Expecting put(stream, ...) to handle blobs over 25 MiB. The platform currently rejects them; multipart upload is not supported yet.
  • ❌ Adding --yes to wdl r2 objects delete proactively. head or list first, and confirm with the user.
  • ❌ Assuming worker deletion cleans up R2. It does not — R2 outlives the worker.

End-to-end example

../examples/inspection-demo — R2 + D1 + KV + assets combined in one project.

Related

  • kv.md — small key-value storage.
  • d1.md — relational data.
  • assets.md — static files served via CDN.