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:
- authenticate the application user;
- list the objects under the requested bucket and prefix;
- authorize the request against those specific objects; and
- return short-lived signed URLs, never Wasabi access keys.
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:
- open the bucket, then Settings → Permissions;
- add a CORS configuration such as:
[
{
"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
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
GetObjectCommandURL, 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.
Download Backblaze B2 Files as a ZIP
List a Backblaze B2 folder, presign each object with the S3-compatible API, and package the results into one ZIP in the browser.
Download MinIO Objects as a ZIP
List objects in a self-hosted MinIO bucket, presign each one, and package the results into one ZIP with @eazip/core.