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.

← All articles