cmd / r2

r2 is a backup tool and a Cloudflare R2 client. It snapshots SQLite databases and bundles git repositories, and it puts, gets, and lists objects. I wrote it in Go with the standard library only.

Why I wrote it

My VM holds the primary copy of my applications, and the provider sells no disk snapshots. A disk failure would take the databases and the repositories with it, so the copies have to live off the box.

The AWS SDK covers that in four API calls and a large dependency. I wrote the four calls instead.

Commands

r2 backup sqlite db.db  # gzipped snapshot, uploaded as db-<UTCts>.db.gz
r2 backup git repo/     # bundle of the branch, as repo-<UTCts>.bundle
r2 put file [key]       # upload a file
r2 get key [dest]       # download an object
r2 ls [prefix]          # list keys with sizes and times

The SQLite backup shells out to sqlite3 .backup, so the snapshot stays consistent while the application writes. I describe the database options in go / sqlite. The git backup shells out to git bundle, and git clone restores the bundle.

Signing requests from the spec

R2 speaks the S3 API, and an S3 request carries a Signature Version 4 signature. The SDK hides that work. r2 implements it from the AWS documentation in 123 lines.

The signer builds a canonical request from the method, path, query, and headers, then hashes it into a string to sign. It derives the signing key through four HMAC rounds: date, region, service, and aws4_request. It signs the string and sets the Authorization header. R2 names its region auto.

AWS publishes test vectors for SigV4, and the repository keeps them under testdata/. Each vector fixes the request and the clock, and the test compares the signature byte for byte. A signer that passes the vectors reaches R2 on the first request.

Design

One PUT per object, and no multipart upload. A single PUT carries a file up to 5 GB, and my artifacts are megabytes.

No pruning logic. A lifecycle rule on the bucket expires an object after 30 days, so retention lives next to the data.

One bucket per VM, with each application as a key prefix: sports/db-<ts>.db.gz, sports/repo-<ts>.bundle, r2/repo-<ts>.bundle. The API token accepts requests from the VM addresses only.

Four environment variables configure the tool: R2_ENDPOINT, R2_BUCKET, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY. An optional R2_PREFIX prepends the application prefix to a generated key. The tool exits when one of the four is missing.

How sports uses it

The scoreboard backs up through a shell script that calls r2 put. Three triggers fire the script: a git post-commit hook after each commit, the application after a clean ingest or a saved summary, and an hourly cron job as the floor. A commit reaches the bucket in seconds.

A restore reads the most recent bundle and snapshot:

r2 ls sports/
r2 get sports/repo-<ts>.bundle
git clone repo-<ts>.bundle sports

What it is not

r2 is not a sync tool and not a log streamer. Litestream ships each write-ahead log frame and restores to any second. r2 ships point-in-time artifacts and restores to the last trigger. I tolerate a recovery point of minutes, so I run the simpler tool.

← All articles