Zig 0.17.0

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

zig
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 u8
  • pub_url: []const u8 = "": Base address of the bucket's public domain, without a trailing slash. Only publicUrl uses it.
  • region: []const u8 = "auto": The region put in the signature. R2 uses auto.

spider.r2.R2

zig
pub const R2 = struct

A client for one R2 bucket. Create it once at startup and share it.

zig
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 to init.
  • client: pacman.Client: Persistent: created once in init()/initFromEnv() and used by every put/get/delete/head/copyObject call. get, head and copyObject reuse its pooled connections; put and delete send Connection: close, so each of them opens a connection of its own.

r2.R2.init

zig
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

zig
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

zig
pub fn deinit(self: *R2) void

Closes the HTTP client and its pooled connections.

r2.R2.put

zig
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

zig
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

zig
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

zig
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

zig
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

zig
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

zig
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.

zig
const url = try store.presignedGet(c.arena, key, 300);

Allocation and errors as in presignedPut.

r2.R2.publicUrl

zig
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

zig
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.