r2 (object storage)
On this page:
- spider.r2: Cloudflare R2 (S3-compatible) object storage:
put,get,delete, presigned URLs. Opt-in with-Dr2=true.
spider.r2
Cloudflare R2 object storage (spider.r2, build option -Dr2): put, get, delete, head and copy objects of one bucket over R2's S3 API, and build presigned URLs so a browser uploads or downloads directly. Requests are signed with AWS Signature V4.
spider.r2.R2Config
pub const R2Config = struct
The bucket and the credentials of an R2 API token. The strings are not copied: they must stay valid while the R2 is in use.
Fields
account_id: []const u8: The Cloudflare account id: the host is<account_id>.r2.cloudflarestorage.com.access_key: []const u8: Access key id of the R2 API token.secret_key: []const u8: Secret access key of the R2 API token.bucket: []const u8pub_url: []const u8 = "": Base address of the bucket's public domain, without a trailing slash. OnlypublicUrluses it.region: []const u8 = "auto": The region put in the signature. R2 usesauto.
spider.r2.R2
pub const R2 = struct
A client for one R2 bucket. Create it once at startup and share it.
var store = try spider.r2.R2.initFromEnv(io);
defer store.deinit();
// in a handler
try store.put(c, "reports/2026-10.pdf", pdf, "application/pdf");
const url = try store.presignedGet(c.arena, "reports/2026-10.pdf", 300);
put, get, delete, head and copyObject call R2 and take the request's Ctx: they use its Io and allocate in c.arena. The presigned URL functions and publicUrl make no network call. A key may contain /; it is percent-encoded for the request.
Fields
config: R2Config: The configuration given toinit.client: pacman.Client: Persistent: created once in init()/initFromEnv() and used by every put/get/delete/head/copyObject call.get,headandcopyObjectreuse its pooled connections;putanddeletesendConnection: close, so each of them opens a connection of its own.
r2.R2.init
pub fn init(io: std.Io, config: R2Config) !R2
Creates the client for config. error.R2ConfigMissing when the account id, a key or the bucket is empty (the log says which). It makes no network call, so wrong credentials show up on the first operation. Free it with deinit. The HTTP client is allocated with std.heap.smp_allocator: create one R2 per process rather than one per request.
r2.R2.initFromEnv
pub fn initFromEnv(io: std.Io) !R2
init with the configuration read from the environment (or .env): R2_ACCOUNT_ID, R2_ACCESS_KEY, R2_SECRET_KEY, R2_BUCKET and R2_PUBLIC_URL. error.R2ConfigMissing when one of the first four is not set; R2_PUBLIC_URL is optional. The region is auto.
r2.R2.deinit
pub fn deinit(self: *R2) void
Closes the HTTP client and its pooled connections.
r2.R2.put
pub fn put(self: *R2, c: *Ctx, key: []const u8, body: []const u8, content_type: []const u8) !void
Uploads body as the object key, with content_type as its Content-Type. An object already at key is replaced. Fails with error.R2PutFailed when R2 answers anything but 200 or 204 (the status and R2's answer are logged), or with the HTTP client's error when R2 cannot be reached.
r2.R2.copyObject
pub fn copyObject(self: *R2, c: *Ctx, source_key: []const u8, dest_key: []const u8) !void
Copies source_key to dest_key inside the bucket, on R2's side: the content is not downloaded. An object already at dest_key is replaced. Fails with error.R2CopyFailed on any answer other than 200 (a missing source included; the status and R2's answer are logged), or with the HTTP client's error when R2 cannot be reached.
r2.R2.get
pub fn get(self: *R2, c: *Ctx, key: []const u8) ![]u8
Downloads the object key and returns its whole content, allocated in c.arena. Fails with error.NotFound when there is no such object (a 404 if a handler lets it through), error.R2GetFailed on any other status than 200, or the HTTP client's error.
r2.R2.delete
pub fn delete(self: *R2, c: *Ctx, key: []const u8) !void
Deletes the object key. Fails with error.NotFound when R2 answers 404, error.R2DeleteFailed on any other status than 200 or 204 (logged, like put and get), or the HTTP client's error.
r2.R2.head
pub fn head(self: *R2, c: *Ctx, key: []const u8) !bool
Whether the object key exists: true on 200, false on 404. The content is not downloaded. Any other status fails with error.R2HeadFailed, and a failure to reach R2 with the HTTP client's error.
r2.R2.presignedPut
pub fn presignedPut(self: *const R2, allocator: std.mem.Allocator, key: []const u8, content_type: []const u8, expires_sec: u32) ![]const u8
A URL that lets its holder upload the object key with a PUT, for expires_sec seconds from now, without credentials. The upload must send a Content-Type header equal to content_type: it is part of the signature. Nothing is sent to R2 and the bucket does not change until the URL is used.
The URL is allocated with allocator and is the caller's; nothing else is left allocated. Fails with error.OutOfMemory.
r2.R2.presignedGet
pub fn presignedGet(self: *const R2, allocator: std.mem.Allocator, key: []const u8, expires_sec: u32) ![]const u8
A URL that lets its holder download the object key with a GET, for expires_sec seconds from now, without credentials. Nothing is sent to R2, so a URL is returned for a key that does not exist too.
const url = try store.presignedGet(c.arena, key, 300);
Allocation and errors as in presignedPut.
r2.R2.publicUrl
pub fn publicUrl(self: *const R2, allocator: std.mem.Allocator, key: []const u8) ![]const u8
config.pub_url, a / and key, allocated with allocator. The key is percent-encoded as a path (its / stay); the object is not checked. Only useful for a bucket with a public domain.
r2.R2.objectKey
pub fn objectKey(self: *const R2, allocator: std.mem.Allocator, tenant_id: []const u8, category: []const u8, filename: []const u8) ![]const u8
Builds the key tenant_id/category/filename, allocated with allocator. The parts are joined as given, without validation.