Fetch your Google Takeout out of Google Drive and into Cloudflare R2 โ automatically, serverless, and with zero egress fees.
A doggy bag is the takeout you carry home. doggybot is the little bot that
carries your Google Takeout home: point it at a Google Drive folder, and on a
schedule it streams every new export into an R2 bucket and clears it out of
Drive โ so your scheduled Takeout backups stop filling up your Drive, and land
somewhere durable and cheap instead.
If you set up a scheduled Google Takeout (Settings โ every 1โ2 months) with delivery to Drive, Google drops big multi-gigabyte zips into your Drive every cycle โ and they pile up against your storage quota until you manually download and delete them. The existing options each miss something:
- Downloading by hand through a browser is slow and easy to get wrong.
google-takeout-sucksand similar scripts download to local disk โ you need a machine with room for the whole export.- rclone works, but needs an always-on box and pays egress on every byte it moves.
doggybot is the piece that was missing: serverless and zero-egress.
doggybot is a single Cloudflare Worker that
you deploy into your own Cloudflare account:
- A cron trigger lists your Drive for new Takeout archives (with a settle guard, so it never grabs an export that's still uploading).
- For each new file it starts a Cloudflare Workflow โ durable, resumable execution โ that streams the file from Drive into R2 with a multipart upload, one ranged chunk at a time. Nothing is ever buffered whole; a crash resumes mid-file instead of restarting it.
- When the object is safely in R2 (verified byte-for-byte), it trashes the file in Drive (recoverable for ~30 days), freeing your quota.
Because the Worker writes to R2 through the R2 binding โ inside the same
Cloudflare account โ the transfer is internal: no per-gigabyte egress
charge, on either leg. DriveโWorker is free ingress; WorkerโR2 is free
internal. You bring your own Google OAuth client and your own R2 bucket, so
your tokens and your data never touch anyone else's servers โ doggybot is
code you run, not a service someone else operates.
- A Cloudflare account (the free plan is enough for a personal backup; R2 storage past the free 10 GB bills at ~$0.015/GB-month, still with zero egress). Workflows are included on the Workers free plan.
- An R2 bucket (you create it;
doggybotwrites to it). - A Google Cloud OAuth client (Desktop type) with the Drive scope, for your own Google account.
- Bun (or npm) and Cloudflare's
wranglerCLI, locally.
git clone https://github.com/caseymrm/doggybot
cd doggybot
bun installbunx wrangler r2 bucket create doggybot-takeoutUse any name you like; if you change it, update bucket_name under
r2_buckets in wrangler.jsonc to match.
- In the Google Cloud Console, create (or reuse) a project.
- APIs & Services โ Enable APIs โ enable the Google Drive API.
- APIs & Services โ OAuth consent screen โ configure it (User type External is fine for a personal account). While it is in Testing, add your own Google account under Test users. (Test-mode refresh tokens expire after 7 days โ publish the app to Production for a token that lasts. You don't need Google verification for a personal, unlisted app.)
- APIs & Services โ Credentials โ Create Credentials โ OAuth client ID โ Application type: Desktop app. Note the Client ID and Client secret.
doggybot runs unattended, so it needs a long-lived refresh token. A small
zero-dependency helper runs the standard Desktop loopback flow on your machine
and prints one โ the token is never written to disk:
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com \
GOOGLE_CLIENT_SECRET=your-secret \
bun run get-refresh-tokenIt opens your browser, you approve access, and it prints the refresh token.
Edit wrangler.jsonc:
- set
vars.GOOGLE_CLIENT_IDto your client id (not sensitive โ safe to commit in your own fork); - optionally set
vars.DRIVE_QUERYto restrict to one folder ("'YOUR_FOLDER_ID' in parents and trashed = false"), tuneR2_KEY_PREFIX, or adjustSETTLE_AGE_SECONDS.
Then store the two secrets (these go into Cloudflare, never the repo):
bunx wrangler secret put GOOGLE_CLIENT_SECRET # paste the client secret
bunx wrangler secret put GOOGLE_REFRESH_TOKEN # paste the token from step 4bun run deployThat's it. On the next cron tick (or immediately, if you enable the scan
endpoint below), doggybot starts moving settled Takeout zips into R2.
The Worker's cron (23 */6 * * * โ at :23 every 6 hours, editable under
triggers.crons in wrangler.jsonc) lists your Drive for matching files and,
for each new, settled one, spawns a per-file Workflow.
- Idempotency. Each Workflow instance's id is the Drive file id. Cloudflare rejects a second instance with the same id, so a file already in-flight is never started twice โ no locks, no state to keep. If a previous run uploaded a file to R2 but crashed before trashing it, the next discovery pass notices the object is already present at the right size, trashes the Drive original, and skips the re-upload.
- The settle guard. Google delivers a large export as many zips, file by
file, with no "done" manifest.
doggybotgroups zips by their export timestamp and waits until a batch's newest file has been untouched forSETTLE_AGE_SECONDS(default one hour) before transferring any of it โ so it never grabs an export that's still being written. - Verify before trash. Every transfer confirms the staged R2 object exists and is exactly the expected size before the Drive original is trashed. Trash is only ~30-day reversible, so nothing is deleted on an unverified copy.
To trigger discovery on demand (handy for testing) instead of waiting for the
cron, set a control token and POST to /scan:
bunx wrangler secret put CONTROL_TOKEN # any random string
curl -X POST https://doggybot.<your-subdomain>.workers.dev/scan \
-H "Authorization: Bearer <that token>"It returns a JSON summary of the pass. Without CONTROL_TOKEN set, /scan is
disabled (404). GET / is a public health check.
- Live logs:
bunx wrangler tailstreams the Worker. Discovery logs a one-line summary each pass (created,skippedSettling,skippedInFlight,finishedAlreadyStaged,errors); each completed file logsstaged โฆ โ r2:โฆ ; trashed from Drive. - Workflow instances: the Cloudflare dashboard (Workers & Pages โ your
Worker โ Workflows) shows every per-file instance and its step timeline; a
terminal failure lands an instance in
erroredwith the failing step.bunx wrangler workflows instances list doggybot-takeout-transferdoes the same from the CLI. - Observability is enabled in
wrangler.jsonc, so logs are queryable in the dashboard without extra setup.
doggybot promises zero egress, not zero cost โ to be honest up front:
- Moving the data (DriveโWorkerโR2) costs nothing: Drive ingress is free, and WorkerโR2 is a free internal write. This is the thing that racks up bills on egress-charging setups, and here it is $0.
- Storing it is what you pay for: R2 is free up to 10 GB, then ~$0.015/GB- month, with no egress fees when you later read it back. A one-time bootstrap of a large photo library is real storage; incremental monthly exports are small.
- Compute for a personal backup fits comfortably in the Workers free plan (a handful of cron ticks and a few Workflow instances per export cycle).
Once a file is transferred, it lives in your R2 bucket under:
<R2_KEY_PREFIX><batchKey>/<originalName>
# e.g. takeout/takeout-20260721t100000z/takeout-20260721T100000Z-001.zip
R2_KEY_PREFIXis thetakeout/prefix you set inwrangler.jsonc.batchKeyis the export's timestamp (takeout-YYYYMMDDTHHMMSSZ), so every zip of one export shares a folder and same-named zips from different exports never collide.- The original Drive
md5Checksum, when Google provides one, is stamped on each object as thedriveMd5custom metadata field for downstream verification.
Because it's your own bucket, read it with whatever you already use โ the
wrangler r2 object get CLI, rclone against an R2 S3 endpoint, the S3 API, or
another Worker with an R2 binding (also zero-egress). A batch is fully present
once every zip of that timestamp shows up under its folder.
Design note.
doggybotdeliberately stops at "the bytes are in your bucket." The private project this engine was extracted from adds a D1-backed ledger and a presigned-URL handoff API so an ingest pipeline can pull whole settled batches; that seam depends on a database and R2 S3 credentials that a standalone backup tool shouldn't need. Keepingdoggybotto the transfer itself means no extra moving parts โ you own the bucket, so you read it directly. If you need the batch-ledger seam, it's a straightforward addition on top of theonFileCompletehook insrc/index.ts.
bun run typecheck # tsc, no emit
bun run test # vitest โ the engine's unit tests
bun run dev # local wrangler devThe transfer engine lives in src/transfer/ (Drive client, OAuth, the durable
Workflow, discovery, batching) and is deployment-agnostic; src/index.ts is the
thin wiring that binds it to your Google credentials and R2 bucket.
MIT ยฉ Casey Muller