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.
MinIO has no "download folder as ZIP" endpoint, so you list the objects under
a prefix on your server, presign each one, and hand the resulting
{ url, filename } list to @eazip/core. Storage credentials stay on the
server; the browser only ever sees short-lived signed URLs and does the
archiving itself.
Loading live demo…
Prepare presigned 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 MinIO access keys.
import { Client } from 'minio';
const minioClient = new Client({
endPoint: 'minio.internal.example.com',
port: 9000,
useSSL: true,
accessKey: process.env.MINIO_ACCESS_KEY!,
secretKey: process.env.MINIO_SECRET_KEY!,
});
type SignedSource = {
url: string;
filename: string;
};
function listObjectKeys(bucket: string, prefix: string): Promise<string[]> {
return new Promise((resolve, reject) => {
const keys: string[] = [];
const stream = minioClient.listObjectsV2(bucket, prefix, true);
stream.on('data', (obj) => {
if (obj.name && !obj.name.endsWith('/')) keys.push(obj.name);
});
stream.on('error', reject);
stream.on('end', () => resolve(keys));
});
}
export async function listPrefixAsSignedUrls(
bucket: string,
prefix: string,
): Promise<SignedSource[]> {
const keys = await listObjectKeys(bucket, prefix);
return Promise.all(
keys.map(async (key) => ({
url: await minioClient.presignedGetObject(bucket, key, 15 * 60),
filename: key.slice(prefix.length),
})),
);
}listObjectsV2 streams results, so it scales to prefixes with many objects
without paging logic of your own. Keep the presignedGetObject expiry (in
seconds; 900 above) short, but long enough for every file to finish
downloading — an expired link only fails that one file, and the archive is
still usable if you leave failOnUrlError at its default.
If your backend already standardizes on @aws-sdk/client-s3, MinIO's S3
compatibility means GetObjectCommand plus getSignedUrl from
@aws-sdk/s3-request-presigner works too — point endpoint at your MinIO
server and set forcePathStyle: true.
Allow browser requests (CORS)
The browser fetches each signed URL directly from MinIO (or from whatever reverse proxy sits in front of it), so that origin must send CORS headers allowing your application's origin.
MinIO Community Edition applies CORS at the server level, not per bucket:
mc admin config set myminio api cors_allow_origin="https://app.example.com"
mc admin service restart myminioThe equivalent MINIO_API_CORS_ALLOW_ORIGIN environment variable works the
same way if you configure MinIO through its container or systemd environment
instead of mc admin config set. Per-bucket CORS rules set with mc cors set exist only in MinIO's commercial AIStor edition — don't rely on that
command against a self-hosted Community Edition server.
Many self-hosted deployments instead put MinIO behind nginx, Traefik, or another reverse proxy. In that case, set the CORS headers on the proxy instead:
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET' always;Without one of these in place, 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();
const result = await createZip({
files,
zipName: 'minio-export.zip',
});
result.download();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. 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;
- MinIO can't be given browser CORS, for example a locked-down internal deployment;
- 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: 'minio-export.zip',
});
result.downloadAll();Add your MinIO 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.
Self-hosted MinIO must be publicly reachable
Eazip Cloud fetches source URLs from its own servers, not from your visitor's browser. If your MinIO instance sits behind a firewall or VPN with no public endpoint, Cloud cannot reach it — Local remains the only option unless you expose a public, authenticated path to those objects.
See When to use Eazip Cloud for the full comparison with Local.
Limits and alternatives
Eazip is built for turning a browser-facing export into one ZIP, not for
operator-driven data movement. If you're an administrator moving objects
rather than serving an end-user download, mc mirror or mc cp --recursive
copies a bucket or prefix straight to local disk without a browser at all —
reach for those first.
For end-user downloads, a Local job is limited by the browser tab's memory
and the tab staying open. Because MinIO is self-hosted, there's no fixed
per-GB egress price like a public cloud provider — cost and throughput
depend on your own server, network, and any proxy or CDN in front of it. For
anything multi-gigabyte, thousands of objects, or that needs to survive a
reload, use strategy: 'cloud' as shown above.
FAQ
Can I download a MinIO bucket or folder as a ZIP without a server?
Not safely for private data. A "folder" in MinIO is just a shared key prefix, and listing or reading it requires access keys or a signed URL, so some backend step has to authenticate the request and presign the objects. If the prefix is fully public and anonymous access is enabled, you could generate the URL list once and cache it, but the listing step still needs credentials somewhere.
Does MinIO support ZIP downloads natively?
No. MinIO stores and serves individual objects and has no operation that returns a prefix as a single archive. The MinIO Console lets a logged-in operator zip objects for their own download, but that's a console feature, not an API your application can call for end users. Eazip does the archiving in the browser or in Eazip Cloud instead.
Why does my MinIO ZIP download fail with a CORS error?
The browser is fetching each signed URL directly from MinIO (or your reverse
proxy), and nothing in that path is sending an Access-Control-Allow-Origin
header for your app's origin. Set cors_allow_origin on the MinIO server, or
add CORS headers at the reverse proxy, as shown above.
Can Eazip Cloud reach a self-hosted MinIO behind my firewall?
Only if you expose a public, authenticated endpoint for it. Cloud fetches source URLs from its own infrastructure, not the visitor's browser, so a MinIO instance reachable only over a private network or VPN is invisible to it. Use Local for those deployments.
Do presigned MinIO URLs expose my access keys?
No. A presigned URL grants time-limited access to one object, signed with your server's access key and secret key; those credentials never reach the browser. Keep the expiry as short as your download flow allows.
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.
Download Vercel Blob Files as a ZIP
List a user's Vercel Blob files with list(), then zip and download them in the browser with Eazip — no server egress or storage credentials client-side.