Skip to content
On this page

Automatic R2 backup and versioning ​

Automatically backup and version files in r2 buckets using cloudflare queues, notifications and a worker.

Setup ​

Create a worker ​

bash
npm create cloudflare@latest

Create a queue ​

bash
npx wrangler queues create my-r2-backup-queue

Create a bucket ​

bash
npx wrangler r2 bucket create my-r2-backup-bucket-1
# or
npx wrangler r2 bucket create my-r2-backup-bucket-1 -s InfrequentAccess

Note that writing to an infrequent access bucket also charges for reads which is charged at $9/month minimum, even for 1 read.

Add to wrangler ​

toml

[[queues.consumers]]
queue = "my-r2-backup-queue"
# Required: this should match the name of the queue created above
# If you misspell the name, you will receive an error when attempting to publish your Worker.
max_batch_size = 1 # optional: defaults to 10
max_batch_timeout = 15 # optional: defaults to 5 seconds

[[r2_buckets]]
binding = "R2_backup_bucket"
bucket_name = "my-r2-backup-bucket-1"

Create worker for backup ​

Using hono for fetch, but only the queue part is required. The entries in buckets json and Environment keys are added for each bucket you want to backup. See below for details about that.

typescript
const cfAccountId = '' // Set your cloudflare account id here.

type Environment = {
    readonly R2_BUCKET_1: R2Bucket
    readonly R2_BUCKET_2: R2Bucket
    readonly R2_BUCKET_3: R2Bucket
    // ...
    
    readonly R2_backup_bucket: R2Bucket
}
const buckets = [
    {
        binding: 'R2_BUCKET_1',
        bucket: 'BUCKET_NAME',
        key: 'my-bucket-1', // key in backup bucket
    },
    {
        binding: 'R2_BUCKET_2',
        bucket: 'BUCKET_NAME',
        key: 'my-bucket-2',
    },
    {
        binding: 'R2_BUCKET_3',
        bucket: 'BUCKET_NAME',
        key: 'my-bucket-3',
    },
]

const app = new Hono<{
    Bindings: Environment
}>()
app.get('/', (c) => {
    return c.text('Nothing here.')
})

export default {
    fetch: app.fetch,
    async queue(batch: MessageBatch<any>, env: Environment) {
        for (const message of batch.messages) {
            if(cfAccountId && message.body.account !== cfAccountId) {
                console.error('Account not found', message.body.account)
                continue;
            }
            const action = message.body.action
            if(action === 'DeleteObject') break;
            const bucketName = message.body.bucket
            const bucket = buckets.find((b) => b.bucket === bucketName)
            if(!bucket) {
                console.log('Bucket not found', bucketName)
                continue;
            }
            const object = message.body.object
            if(!object) {
                console.log('Object not found', bucketName)
                continue;
            }
            const timeKey = new Date(message.body.eventTime).toISOString().replace(/:/g, '-')
            const key = `${bucket.key}/${object.key}/${timeKey}`
            const eTag = object.eTag
            const r2Bucket = env[bucket.binding]
            if(!r2Bucket) {
                console.error('Misconfiguration', bucket.binding)
                continue
            }
            const obj = await r2Bucket.get(object.key)
            if(!obj) {
                console.log('Object not found', key)
                continue
            }
            if(obj?.etag !== eTag) {
                console.log('Mismatch', key)
                continue;
            }
            const res = await env.R2_backup_bucket.put(key, obj.body, {
                httpMetadata: obj.httpMetadata,
                customMetadata: obj.customMetadata,
                storageClass: "InfrequentAccess",
            })
            console.log('Backup', bucketName, object.key, res.key)
        }
    },
}

Add buckets for backup ​

Do the following for each bucket you want to backup.

Create notifications ​

bash
npx wrangler r2 bucket notification create BUCKET_NAME --event-type object-create --queue ij-drive-r2-backup-version-1

Docs - https://developers.cloudflare.com/workers/wrangler/commands/#notification-create

To check the notifications -

bash
npx wrangler r2 bucket notification list BUCKET_NAME

Add to wrangler ​

toml
[[r2_buckets]]
binding = "R2_BUCKET_1"
bucket_name = "BUCKET_NAME"

Add to json in worker ​

json5
{
    binding: "R2_BUCKET_1", // binding in wrangler.toml
    bucket: "BUCKET_NAME", // actual bucket name
    key: "bucket-name", // key(folder) in backup bucket
}

Made with ❤️ using the awesome vitepress