I have a list of files stored in R2 and I want to serve them bundled as a Zip file to my user, but I want to do this through Cloudflare Workers and all in memory so it never hits even temporary storage.
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:
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 somefile of payload.data.files) {
const { r2key, filename } = somefile;
// Fetch the file 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 file 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
.
Leave a Reply