Eazip
Guides

Download Wasabi Files as a ZIP

Presign Wasabi Hot Cloud Storage objects on the server, then package them into one ZIP in the browser with Eazip — no storage credentials client-side.

Wasabi is S3-compatible, so there's no native "download folder as ZIP" call: you list the objects under a prefix, presign each one with the AWS SDK against Wasabi's endpoint, and hand the resulting { url, filename } list to @eazip/core. Wasabi credentials stay on your server, and the browser does the archiving.

Loading live demo…

Prepare presigned URLs on the server

Point S3Client at your bucket's Wasabi region endpoint instead of AWS. Your backend endpoint should:

  1. authenticate the application user;
  2. list the objects under the requested bucket and prefix;
  3. authorize the request against those specific objects; and
  4. return short-lived signed URLs, never Wasabi access keys.
server/export.ts
import {
  S3Client,
  ListObjectsV2Command,
  GetObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  region: 'us-east-1',
  endpoint: 'https://s3.us-east-1.wasabisys.com',
  credentials: {
    accessKeyId: process.env.WASABI_ACCESS_KEY_ID!,
    secretAccessKey: process.env.WASABI_SECRET_ACCESS_KEY!,
  },
});

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

export async function listPrefixAsSignedUrls(
  bucket: string,
  prefix: string,
): Promise<SignedSource[]> {
  const sources: SignedSource[] = [];
  let continuationToken: string | undefined;

  do {
    const page = await s3.send(
      new ListObjectsV2Command({
        Bucket: bucket,
        Prefix: prefix,
        ContinuationToken: continuationToken,
      }),
    );

    for (const object of page.Contents ?? []) {
      if (!object.Key || object.Key.endsWith('/')) continue;

      const url = await getSignedUrl(
        s3,
        new GetObjectCommand({ Bucket: bucket, Key: object.Key }),
        { expiresIn: 900 },
      );

      sources.push({ url, filename: object.Key.slice(prefix.length) });
    }

    continuationToken = page.NextContinuationToken;
  } while (continuationToken);

  return sources;
}

The endpoint must match the bucket's storage region exactly, for example s3.us-east-1.wasabisys.com or s3.eu-central-1.wasabisys.com; a mismatched region rejects the request. ListObjectsV2Command still returns at most 1,000 keys per page, so the ContinuationToken loop matters for larger folders. Wasabi accepts a presigned URL for up to 7 days, but keep expiresIn close to how long the ZIP build actually takes.

Configure bucket CORS

The browser fetches each signed URL directly from Wasabi, so the bucket must allow your app's origin. Wasabi returns permissive CORS headers by default — Access-Control-Allow-Origin: * — whenever a request carries an Origin header, which is enough for most Local jobs without any extra setup.

If the bucket has a stricter, custom CORS policy, add one from the Wasabi Console instead of the S3 PutBucketCors API, which Wasabi does not support:

  1. open the bucket, then Settings → Permissions;
  2. add a CORS configuration such as:
wasabi-cors.json
[
  {
    "AllowedOrigins": ["https://app.example.com"],
    "AllowedMethods": ["GET"],
    "AllowedHeaders": ["*"],
    "MaxAgeSeconds": 3000
  }
]

A custom policy replaces Wasabi's permissive default, so include every origin your app serves from. Without a matching rule, the browser blocks the fetch and Eazip reports the file as failed even though the signed URL is valid.

Create the ZIP in the browser

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

const response = await fetch('/api/exports/bucket-prefix', {
  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: 'wasabi-export.zip',
});

result.download();

files also accepts plain URL strings or a mix of URLs and browser File or Blob objects; see Input types for every accepted shape. createZip() resolves once every reachable object has been fetched and packaged, then result.download() starts the browser download. Use startZip() instead when you need live progress or a cancel button; see Create a ZIP from remote URLs for that shape and for how partial results behave when some objects fail.

When to use Eazip Cloud

Local jobs like the one above run entirely in the visitor's tab. Switch to strategy: 'cloud' when:

  • the prefix totals multiple gigabytes or thousands of objects;
  • a custom CORS policy on the bucket can't allow your app's origin;
  • the export should survive a page reload; or
  • you would rather not hold archive bytes in tab memory at all.

Cloud accepts the same signed URL list; only the strategy changes:

const result = await createZip({
  strategy: 'cloud',
  publicKey: 'pk_ez_...',
  files,
  zipName: 'wasabi-export.zip',
});

result.downloadAll();

Add the bucket's Wasabi hostname to the Public App's allowed source hosts, and make the presigned URL lifetime long enough for Eazip Cloud to start and finish reading each object. See When to use Eazip Cloud for the full comparison with Local.

Limits and alternatives

Wasabi advertises free egress, but it's governed by a fair-use policy, not an unlimited allowance: monthly downloads should stay roughly in line with what you have stored, up to a stated cap, and sustained traffic well above that ratio can lead Wasabi to reach out about your usage or throttle it. Zipping a folder still means fetching every object once, so that traffic counts toward your account's usage the same as any other download — check your Wasabi plan before scripting very large or frequent bulk exports.

Eazip is not the right tool for every Wasabi download:

  • A single object doesn't need a ZIP at all; return one presigned GetObjectCommand URL, or generate one from Wasabi Explorer, and let the browser download it directly.
  • A full bucket mirror on disk, rather than one ZIP a user downloads, is a better fit for the AWS CLI, rclone, or Wasabi's own sync tools against the same S3-compatible endpoint.

FAQ

How do I download a Wasabi folder as a ZIP?

List the objects under that prefix on your server, presign each with getSignedUrl against your bucket's Wasabi endpoint, and pass the resulting { url, filename } list to createZip in the browser. See Prepare presigned URLs on the server above.

How do I generate a presigned URL for a Wasabi object?

Use @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner exactly as you would for AWS S3, but point S3Client at Wasabi's region endpoint, such as https://s3.us-east-1.wasabisys.com, with your Wasabi access key and secret. Wasabi Explorer, the AWS CLI's presign command, and S3 Browser can also generate one manually for a single object.

Why does my Wasabi ZIP download fail with a CORS error?

Either the bucket has a custom CORS policy that doesn't list your app's origin, or a signed URL expired before the browser fetched it. Check the bucket's Settings → Permissions CORS configuration in the Wasabi Console, and confirm AllowedMethods includes GET for your exact origin.

How large a Wasabi bucket can Eazip zip?

A Local job is limited by the browser tab's memory and the tab staying open. For anything multi-gigabyte, thousands of objects, or that needs to survive a reload, use strategy: 'cloud' as shown above.

Do signed URLs expose my Wasabi access keys?

No. A presigned URL grants time-limited access to one object using your server's Wasabi credentials to sign it; the credentials themselves never reach the browser. Keep expiresIn as short as your download flow allows, and no longer than Wasabi's 7-day maximum.