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
The VM holds primary copies of databases and repositories. Without host-level disk snapshots, backups must leave the machine. The AWS SDK adds a large dependency tree for four API calls, so I wrote them directly.
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. The git backup shells out to git bundle.
Signing requests from the spec
R2 implements the S3 API and requires Signature Version 4. r2
implements SigV4 in 123 lines of Go standard library code. The app-side
client uses the same approach (see go / object storage).
The signer formats a canonical request and hashes it to create the string to sign:
func (s *SigV4) canonicalRequest(method string, u *url.URL, headers map[string]string, contentSHA string) string {
return strings.Join([]string{
strings.ToUpper(method),
canonicalPath(u),
canonicalQuery(u.RawQuery),
canonicalHeaders(headers) + "\n",
signedHeaders(headers),
contentSHA,
}, "\n")
}
Four rounds of HMAC-SHA256 (date, region, service, aws4_request) derive
the signing key. The signer sets the Authorization header. R2 sets the
region to auto.
The repository validates the implementation against AWS SigV4 test
vectors in testdata/.
Design
r2 uploads files up to 5 GB with a single PUT and omits multipart
logic. Bucket lifecycle rules expire objects by prefix: hourly keys
after 7 days, daily keys after 90 days, and weekly keys after 400 days.
Keys carry tier and application prefixes: hourly/sports/db-<ts>.db.gz,
daily/sports/repo-<ts>.bundle. Four environment variables configure the
tool: R2_ENDPOINT, R2_BUCKET, R2_ACCESS_KEY_ID, and
R2_SECRET_ACCESS_KEY.
How sports uses it
The scoreboard backs up through a shell script that
calls r2 put. A git post-commit hook pushes a bundle after each
commit, so a commit reaches the bucket in seconds. The application
snapshots the database on each clean ingest or saved summary, and those
snapshots stay on the VM. An hourly cron job pushes the changed
snapshots off the box. Each push writes the hourly prefix. The first
push of the day also writes the daily prefix, and the first of the week
also writes the weekly prefix.
A restore reads the most recent bundle and snapshot:
r2 ls hourly/sports/
r2 get hourly/sports/repo-<ts>.bundle
git clone repo-<ts>.bundle sports
The rehearsal
A rehearsal runs on the VM each Wednesday. It downloads the newest snapshot and bundle, restores the database, and verifies the key and byte count.
git / restore rehearsals describes the recovery verification for sockeye git repositories.
Compared to Litestream
Litestream replicates SQLite write-ahead log
frames continuously. r2 ships point-in-time snapshots and bundles on
triggers. This provides simple, predictable backups.