Fetch file listings and individual files from a remote ZIP file.
Without downloading the entire ZIP:
The gist of what the library does is:
Decrypting WinZip AES entries uses the Web Crypto API (
crypto.subtle), which in browsers is only available in a secure context (HTTPS orlocalhost). Traditional ZipCrypto works everywhere but is cryptographically weak.
npm install @gyng/remote-zip
yarn add @gyng/remote-zip
See the generated API documentation.
The remote server must support HTTP Range requests (respond 206 Partial Content). When used cross-origin from a browser, it must also send the right
CORS headers. The browser issues the CORS preflight (OPTIONS) automatically —
there is nothing to configure on the client — but the server needs to allow it:
Access-Control-Allow-Origin: <your origin>Access-Control-Allow-Methods: GET, HEAD (add POST if you set a custom method)Access-Control-Allow-Headers: Range (plus Authorization / any custom headers you pass)Access-Control-Expose-Headers: Content-Length, Content-Range — required, or
the browser hides those response headers and populate() cannot read the archive sizeRange is not a CORS-safelisted request header, so any cross-origin request
triggers a preflight. Static hosts like S3, GitHub Pages, and nginx support Range
out of the box; you only need to configure the CORS headers above.
const url = new URL("http://www.example.com/test.zip");
const remoteZip = await new RemoteZipPointer({ url }).populate();
const fileListing = remoteZip.files(); // RemoteZipFile[]
const uncompressedBytes = await remoteZip.fetch("test.txt"); // Uint8Array
For large entries, fetchStream returns a ReadableStream<Uint8Array> of the
uncompressed bytes so you can process them incrementally without buffering the
whole file. maxUncompressedSize, if set, is enforced mid-stream.
const stream = await remoteZip.fetchStream("big.bin");
for await (const chunk of stream) {
// handle each chunk
}
const additionalHeaders = new Headers();
additionalHeaders.append("X-Example", "foobar");
const url = new URL("http://www.example.com/test.zip");
const remoteZip = await new RemoteZipPointer({
url,
additionalHeaders,
method: "POST",
credentials: "include",
// New request options (all optional), applied to every request:
redirect: "error", // avoid leaking auth headers cross-origin on a 30x
timeoutMs: 10_000, // per-request timeout
signal: AbortSignal.timeout(30_000), // or your own AbortController signal
requestInit: { cache: "no-store" }, // escape hatch merged into every fetch
}).populate();
// Guard untrusted archives against decompression bombs, and pass a per-call
// signal/timeout if you like:
const uncompressedBytes = await remoteZip.fetch("test.txt", additionalHeaders, {
maxUncompressedSize: 50 * 1024 * 1024,
verifyCrc: true, // check the decompressed bytes against the entry's CRC-32
password: "hunter2", // for ZipCrypto / WinZip AES encrypted entries
});
Requires Node >= 22.
npm ci # install (reproducible, honours .npmrc cooldown)
npm run d # watch and build
npm run t:watch # watch and test
npm run lint # prettier + eslint
npm run typecheck # tsc --noEmit (also checks tests)
npm run build # type declarations + esm/cjs bundles
npm test # run the test suite once
Get an automation token from npm under settings
https://www.npmjs.com/settings/$YOUR_USERNAME/tokens/
Add the token to your repository secrets.
https://github.com/$YOUR_USERNAME/$YOUR_REPO_NAME/settings/secrets/actions/new
NPM_TOKENCreate a new release.
https://github.com/$YOUR_USERNAME/$YOUR_REPO_NAME/releases
The workflow at ./github/workflows/publish.yml should run and publish your packages to both NPM and GitHub Packages.
Don't forget to bump your version number in package.json before this.
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.