It seems like a super basic thing you’d want to do with Cloudflare Workers when you store files in R2, but I’ve googled far and wide and couldn’t find an example of this anywhere.
So here’s my take on this, after much trial and error:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { z } from 'zod'; | |
import { ZipWriter, BlobReader, configure } from '@zip.js/zip.js'; | |
// Without this, we get uncaught error due to Workers runtime bug | |
// See: https://github.com/gildas-lormeau/zip.js/discussions/514 | |
configure({ | |
useCompressionStream: false, | |
}); | |
// Payload schema that lists the files to be bundled, their filenames and the archive filename | |
const schema = z.object({ | |
archive_filename: z.string().endsWith('.zip'), | |
files: z.array( | |
z.object({ | |
r2key: z.string(), | |
filename: z.string(), | |
}) | |
), | |
}); | |
export default { | |
async fetch(request, env, ctx): Promise<Response> { | |
const body = await request.json(); | |
const payload = schema.safeParse(body); | |
if (!payload.success) { | |
return new Response(JSON.stringify({ status: 'failed', error: payload.error }), { | |
status: 409, | |
headers: { 'Content-Type': 'application/json' }, | |
}); | |
} | |
let { readable, writable } = new IdentityTransformStream(); | |
// Create a ZIP archive stream | |
const archive = new ZipWriter(writable); | |
// Store all the promises to catch any errors all together later | |
let promises: Promise<any>[] = []; | |
for (const receipt of payload.data.files) { | |
const { r2key, filename } = receipt; | |
// Fetch the receipt from the R2 bucket | |
const fileContent = await env.STORAGE_BUCKET.get(r2key); | |
if (!fileContent) { | |
return new Response(`Object not found: ${r2key}`, { status: 404 }); | |
} | |
// Add the receipt to the ZIP archive | |
promises.push(archive.add(filename, new BlobReader(await fileContent.blob()))); | |
} | |
promises.push(archive.close()); | |
Promise.all(promises).catch((err) => { | |
console.log(err); | |
}); | |
return new Response(readable, { | |
headers: { | |
'Content-Type': 'application/zip', | |
'Content-Disposition': `attachment; filename="${payload.data.archive_filename}"`, | |
}, | |
}); | |
}, | |
} satisfies ExportedHandler<Env>; |
You can just POST to it a JSON object with the output filename for the zip file and a list of R2 object keys with a filename to set for each.
Don’t forget to set your bindings in the wrangler.toml
.