Download an Entire S3 Bucket or Prefix as a ZIP
List a bucket prefix, presign each object, and package the results into one ZIP with @eazip/core in the browser.
S3 has no built-in "download folder as ZIP" action, so you list the objects
under a prefix, presign each one, and hand the resulting { url, filename }
list to @eazip/core. This keeps storage credentials on the server and lets
the browser do the archiving.
Loading live demo…
Prepare signed URLs on the server
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 AWS credentials.
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' });
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;
}ListObjectsV2Command returns at most 1,000 keys per page, so a prefix with
more objects needs the ContinuationToken loop above. Keep expiresIn short,
but long enough for every file to finish downloading; a link that expires
mid-fetch fails only that file, and the archive is still usable if you leave
failOnUrlError at its default.
Configure bucket CORS
The browser fetches each object directly from S3, so the bucket must allow your application's origin:
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}
]Apply it with the AWS CLI or the S3 console's CORS editor:
aws s3api put-bucket-cors \
--bucket your-bucket \
--cors-configuration file://s3-cors.jsonWithout this, the browser blocks the fetch and Eazip reports the file as failed even though the signed URL itself 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();files also accepts plain URL strings, or a mix of URLs and browser File or
Blob objects if part of the export comes from a local picker; see
Input types for every accepted shape.
const result = await createZip({
files,
zipName: 'bucket-export.zip',
});
result.download();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 Eazip Cloud when:
- the prefix totals multiple gigabytes or thousands of objects;
- the bucket cannot allow browser CORS, for example a strict compliance bucket;
- 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: 'bucket-export.zip',
});
result.downloadAll();Add the bucket's 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
Every approach here still pays S3's standard data transfer pricing: objects served to a fetch in the browser, or to Eazip Cloud, count as data transfer out of S3, billed at roughly $0.09/GB after the first 100 GB/month across your AWS account (US regions; other regions and higher tiers price differently). Cloud's stream mode removes the archive from your visitor's browser, but it does not remove that underlying S3 egress cost — someone still fetches every byte from S3 once.
Eazip is not the right tool for every S3 download:
- A single object doesn't need a ZIP at all; return one presigned
GetObjectCommandURL and let the browser download it directly. - A server-side archive, built with a Lambda function that streams
objects into a ZIP and uploads the result, avoids sending bytes through the
visitor's browser, but you take on Lambda's
/tmpstorage limit (up to 10 GB, configurable) and 15-minute maximum execution time yourself. Eazip Cloud exists largely so you don't have to build and operate that path.
FAQ
Can I download an S3 folder as a ZIP without a server?
Not safely for private data. A "folder" in S3 is just a shared key prefix, and listing or reading it requires AWS credentials or a signed URL, so some backend step has to authenticate the request and presign the objects. If the prefix is fully public, you can generate the URL list once and cache it, but the listing step still needs to run somewhere with S3 access.
Does S3 support ZIP downloads natively?
No. S3 stores and serves individual objects; it has no operation that returns a prefix as a single archive. Some tools work around this with S3 Batch Operations or a Lambda function, but both require you to write and run that archiving logic yourself. Eazip does the archiving in the browser or in Eazip Cloud instead.
Why does my S3 ZIP download fail with a CORS error?
The browser is fetching each signed URL directly from S3, and the bucket has
no CORS rule allowing your origin. Add the CORS configuration shown above, and
confirm AllowedMethods includes GET for the app's exact origin.
How large a 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 AWS credentials?
No. A presigned URL grants time-limited access to one object using your
server's credentials to sign it; the credentials themselves never reach the
browser. Keep expiresIn as short as your download flow allows.
Download All Files From Supabase Storage as a ZIP
List a Supabase Storage folder, sign the objects on the server, and package them into one ZIP in the browser with Eazip.
Download Cloudflare R2 Objects as a ZIP
Presign R2 objects on the server, then zip them in the browser with Eazip — no storage credentials client-side and no R2 egress fees.