Download UploadThing Files as a ZIP
Resolve a user's UploadThing uploads into signed URLs on the server, then fetch and zip them in the browser with Eazip.
UploadThing does not have a built-in "download all" action, and users can only
fetch one file at a time from its URLs. Eazip fills that gap: your backend
resolves the files a user owns into { url, filename } entries with UTApi,
and @eazip/core fetches and zips them in the browser.
Loading live demo…
Build the export endpoint
UploadThing's listFiles() returns every file in your app, not the files that
belong to one user. Most apps already solve this by writing the file key and
name to their own database when an upload completes, so the export endpoint
should read from that table rather than from listFiles() directly.
// app/api/exports/uploads/route.ts
import { NextResponse } from 'next/server';
import { UTApi } from 'uploadthing/server';
import { getSessionUser } from '@/lib/auth';
import { getUploadsForUser } from '@/lib/db';
const utapi = new UTApi();
export async function GET(request: Request) {
const user = await getSessionUser(request);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Your own table, written when each upload completes: which UploadThing
// key belongs to which user, and the original filename.
const uploads = await getUploadsForUser(user.id);
const files = await Promise.all(
uploads.map(async (upload) => {
const { ufsUrl } = await utapi.generateSignedURL(upload.key, {
expiresIn: '10m',
});
return { url: ufsUrl, filename: upload.name };
}),
);
return NextResponse.json(files);
}generateSignedURL() is required for files uploaded with a private ACL. If
your app uses the default public-read ACL, you can skip signing and build
the URL directly:
const url = `https://${process.env.UPLOADTHING_APP_ID}.ufs.sh/f/${upload.key}`;Authorize before you list
Only resolve keys that belong to the authenticated user. listFiles() and
the <APP_ID>.ufs.sh URL format expose whatever key you pass them, so the
authorization check belongs in your own database query, not in the ZIP step.
Create the ZIP in the browser
Fetch the export endpoint, hand the resulting list to createZip, and start
the download:
import { createZip } from '@eazip/core';
const response = await fetch('/api/exports/uploads', {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Could not prepare export files');
}
const files = await response.json();
const result = await createZip({
files,
zipName: 'my-uploads.zip',
});
result.download();UploadThing serves files from ufs.sh with CORS headers that allow
cross-origin browser fetches, so a Local job can read them directly without
proxying bytes through your server. Signed URLs must stay valid until Eazip
fetches them, so set expiresIn generously if the archive is large or the
user's connection is slow.
When to use Eazip Cloud
Local jobs cover most exports. Move to Cloud when:
- a user has thousands of uploads or the combined archive is multi-gigabyte;
- the export should keep running if the user closes or reloads the tab;
- you want the ZIP to build without holding file bytes in tab memory; or
- your backend should create the session so the signed URL list never reaches the browser.
const result = await createZip({
strategy: 'cloud',
publicKey: 'pk_ez_...',
files,
zipName: 'my-uploads.zip',
});
result.downloadAll();Add ufs.sh to the Public App's allowed source hosts, and keep each signed
URL's expiresIn long enough for Eazip Cloud to start and finish the job.
Limits and alternatives
listFiles()returns app-wide file metadata, not a per-user file list. Track ownership yourself when an upload completes; do not filterlistFiles()results by trusting client-supplied user IDs.- Signed URLs from
generateSignedURL()expire after at most 7 days, so export links you cache for later use will eventually need to be regenerated. - If a user only has a handful of files, direct per-file download links may be simpler than a ZIP; reserve this workflow for exports of several files or more.
FAQ
Can users download all their UploadThing files at once?
Not directly from UploadThing. Build a server endpoint that resolves the
user's file keys into signed or public URLs, then pass that list to
createZip so the browser fetches and archives them together.
Does UploadThing support bulk downloads natively?
No. UTApi can list and delete files, but it has no ZIP or bulk-export
feature. Eazip handles the archiving step once your endpoint returns the file
URLs.
How do I zip private UploadThing files?
Call utapi.generateSignedURL(key, { expiresIn }) for each file on the
server and pass the resulting ufsUrl values to createZip. Never call
generateSignedURL from browser code, since it requires your UploadThing
secret key.
Will this work if my UploadThing files are public?
Yes. Public (public-read) files can be zipped from their standard
https://<APP_ID>.ufs.sh/f/<key> URL without generating a signed URL first.
What if a user has thousands of uploaded files?
Use strategy: 'cloud' instead of the default Local job. Cloud fetches the
URLs outside the browser tab, so archive size and file count are no longer
limited by tab memory or lifetime.
Next steps
- Create a ZIP from remote URLs covers the general URL-list workflow this guide builds on.
- Input types documents every shape
createZipaccepts, including mixed File and URL lists. - When to use Eazip Cloud explains the full Local versus Cloud trade-off.
Download Vercel Blob Files as a ZIP
List a user's Vercel Blob files with list(), then zip and download them in the browser with Eazip — no server egress or storage credentials client-side.
Create a ZIP From Presigned URLs
Turn any storage provider's expiring signed URLs into one browser ZIP download with @eazip/core, without exposing credentials.