Eazip
Guides

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 filter listFiles() 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