Eazip
Guides

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.

Any storage that issues expiring, signed URLs works the same way: your server authenticates the request and signs each object, and the browser fetches the resulting URLs and zips them with @eazip/core. This pattern applies whether the URLs come from Amazon S3, an S3-compatible provider, or a completely different object store — the only thing that changes is how you generate the URL.

Loading live demo…

The pattern

Every provider that supports presigned or signed URLs needs the same four server-side responsibilities:

  1. authenticate the application user;
  2. authorize the specific objects being requested;
  3. create short-lived signed URLs; and
  4. return only { url, filename } entries — never storage credentials.
server/export.ts
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  region: 'auto',
  endpoint: process.env.STORAGE_ENDPOINT,
});

type SignedSource = {
  url: string;
  filename: string;
};

export async function signSources(keys: string[]): Promise<SignedSource[]> {
  return Promise.all(
    keys.map(async (key) => ({
      url: await getSignedUrl(
        s3,
        new GetObjectCommand({ Bucket: process.env.BUCKET, Key: key }),
        { expiresIn: 900 },
      ),
      filename: key,
    })),
  );
}

Only the endpoint (and sometimes region) changes between providers, because @aws-sdk/s3-request-presigner works against any S3-compatible API. That covers Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Wasabi, MinIO, Hetzner Object Storage, Scaleway Object Storage, OVHcloud Object Storage, and Tigris. Providers with their own SDK — Supabase Storage, Firebase Storage, Azure Blob Storage — use a different signing call, but return the same { url, filename } shape.

Create the ZIP in the browser

Once your endpoint returns signed sources, fetching and zipping them is provider-agnostic:

download-export.ts
import { createZip } from '@eazip/core';

const response = await fetch('/api/exports/123/files', {
  credentials: 'include',
});

if (!response.ok) {
  throw new Error('Could not prepare export files');
}

const files: { url: string; filename: string }[] = await response.json();

const result = await createZip({
  files,
  zipName: 'export.zip',
});

result.download();

createZip() resolves once every reachable URL has been fetched and packaged; result.download() then starts the browser download. files also accepts plain URL strings or a mix of URLs and browser File/Blob objects — see Input types for every accepted shape. For a Local job, the storage provider must allow browser CORS on GET requests from your application's origin, or Eazip reports each blocked file as failed.

Pick the URL lifetime

The signed URL's expiry has to outlive the ZIP job, not just the moment the link is generated:

  • Small jobs (a handful of files) can use a short expiry, such as the 15-minute default shown above — the whole fetch usually finishes in seconds.
  • Large jobs (many files, or large files on a slow connection) need a longer expiry, because the last file is signed once but fetched only after every earlier file has downloaded.
  • A URL that expires mid-job fails only that one file. With failOnUrlError left at its default, createZip() still returns a partial ZIP with the files that succeeded; set failOnUrlError: true if every file is required and a failure should reject the whole job instead.

When in doubt, sign for longer than you think you need. A signed URL that sits unused for a few extra minutes costs nothing; one that expires before the fetch runs turns into a support ticket.

When to use Eazip Cloud

Local jobs run entirely in the visitor's browser tab and cover most everyday downloads. Switch to strategy: 'cloud' when:

  • the archive is multi-gigabyte or contains thousands of signed URLs;
  • the storage provider can't or won't allow browser CORS;
  • the job needs to survive a page reload; or
  • you'd rather not hold archive bytes in tab memory at all.
const result = await createZip({
  strategy: 'cloud',
  publicKey: 'pk_ez_...',
  files,
  zipName: 'export.zip',
});

result.downloadAll();

Cloud fetches the same signed URLs outside the browser, so add the storage provider's hostname to the Public App's allowed source hosts. Stream mode is the default and adds no separate outbound-bandwidth fee on top of Eazip's own metered usage, though the storage provider may still charge for the source fetch; stored mode instead keeps a reusable archive artifact. See When to use Eazip Cloud for the full comparison with Local.

FAQ

How long should presigned URLs live for a ZIP job?

Long enough to outlast the whole job, not just the request that creates them. Size the expiry to your slowest expected download, and remember the last file in the list is fetched only after every earlier one — so a large job needs a longer expiry than a single-file download link.

Do presigned URLs need CORS for browser downloads?

Yes, for Local jobs. The browser fetches each signed URL directly from the storage provider, so the provider must return Access-Control-Allow-Origin for your application's origin on GET requests. Eazip Cloud fetches outside the browser and isn't subject to browser CORS.

Can I zip presigned URLs from any S3-compatible provider the same way?

Yes. @aws-sdk/s3-request-presigner and the AWS SDK's S3Client work against any S3-compatible endpoint, including Hetzner Object Storage, Scaleway Object Storage, OVHcloud Object Storage, and Tigris — point endpoint at the provider and the rest of the signing and zipping code stays the same.

What happens if one signed URL expires before Eazip fetches it?

By default, only that file is skipped; createZip() still resolves with a partial ZIP containing the files that succeeded, and result.status is 'partial'. Set failOnUrlError: true if the job should fail outright instead.

Do I need Eazip Cloud just to use presigned URLs?

No. Presigned URLs work with Local by default — Cloud is only needed once the job outgrows the browser tab, as described above.

Next steps