GithubHelp home page GithubHelp logo

client-zip's Introduction

Test Size Dependencies Types

What is client-zip ?

client-zip concatenates multiple files (e.g. from multiple HTTP requests) into a single ZIP, in the browser, so you can let your users download all the files in one click. It does not compress the files or unzip existing archives.

client-zip is lightweight (6.3 kB minified, 2.6 kB gzipped), dependency-free, and 40 times faster than the old JSZip.

Quick Start

npm i client-zip

(or just load the module from a CDN such as UNPKG or jsDelivr)

For direct usage with a ServiceWorker's importScripts, a worker.js file is also available alongside the module.

import { downloadZip } from "https://cdn.jsdelivr.net/npm/client-zip/index.js"

async function downloadTestZip() {
  // define what we want in the ZIP
  const code = await fetch("https://raw.githubusercontent.com/Touffy/client-zip/master/src/index.ts")
  const intro = { name: "intro.txt", lastModified: new Date(), input: "Hello. This is the client-zip library." }

  // get the ZIP stream in a Blob
  const blob = await downloadZip([intro, code]).blob()

  // make and click a temporary link to download the Blob
  const link = document.createElement("a")
  link.href = URL.createObjectURL(blob)
  link.download = "test.zip"
  link.click()
  link.remove()

  // in real life, don't forget to revoke your Blob URLs if you use them
}

Compatibility

client-zip works in all modern browsers (and Deno) out of the box. If you bundle it with your app and try to transpile it down to lower than ES2020, it will break because it needs BigInts. Version 1.x may be painfully transpiled down to as low as ES2015.

The default release of version 2 targets ES2020 and is a bare ES module + an IIFE version suitable for a ServiceWorker's importScript. Version 1 releases were built for ES2018.

When necessary, client-zip version 2 will generate Zip64 archives. It will always specify "ZIP version 4.5 required to unzip", even when that's not really true. The resulting files are not readable by every ZIP reader out there.

Usage

The module exports three functions:

function downloadZip(files: ForAwaitable<InputTypes>, options?: Options): Response

function makeZip(files: ForAwaitable<InputTypes>, options?: Options): ReadableStream

function predictLength(metadata: Iterable<MetadataTypes>): bigint

downloadZip is obviously the main function and the only one exposed by the worker script. You give it an (async or not) iterable a.k.a ForAwaitable list of inputs. Each input (InputTypes) can be:

  • a Response
  • a File
  • or an object with the properties:
    • name: the file name ; optional if your input is a File or a Response because they have relevant metadata
    • lastModified: last modification date of the file (defaults to new Date() unless the input is a File or Response with a valid "Last-Modified" header)
    • input: something that contains your data; it can be a File, a Blob, a Response, some kind of ArrayView or a raw ArrayBuffer, a ReadableStream<Uint8Array> (yes, only Uint8Arrays, but most APIs give you just that type anyway), an AsyncIterable<ArrayBuffer | ArrayView | string>, … or just a string.

The options argument currently supports three properties, length, metadata (see Content-Length prediction) and buffersAreUTF8 (see Filename encoding).

The function returns a Response immediately. You don't need to wait for the whole ZIP to be ready. It's up to you if you want to pipe the Response somewhere (e.g. if you are using client-zip inside a ServiceWorker) or let the browser buffer it all in a Blob.

Unless your list of inputs is quite small, you should prefer generators (when zipping Files or other resources that are already available) and async generators (when zipping Responses so you can fetch them lazily, or other resources that are generated last-minute so you don't need to store them longer than necessary) to provide the inputs to downloadZip.

makeZip is just like downloadZip except it returns the underlying ReadableStream directly, for use cases that do not involve actually downloading to the client filesystem.

Content-Length prediction

Because of client-zip's streaming design, it can't look ahead at all the files to determine how big the complete archive will be. The returned Response will therefore not have a "Content-Length" header, and that can be problematic.

Starting with version 1.5, if you are able to gather all the relevant metadata (file sizes and names) before calling downloadZip, you can get it to predict the exact size of the archive and include it as a "Content-Length" header. The metadata must be a synchronous iterable, where each item (MetadataTypes) can be :

  • a Response, either from the actual request you will use as input, or a HEAD request (either way, the response body will not be consumed at this point)
  • a File
  • or an object with the properties:
    • name: the file name ; optional if your input is a File or a Response because they have relevant metadata
    • size: the byte length of the file ; also optional if you provide a File or a Response with a Content-Length header
    • input: same as what you'd pass as the actual input, except this is optional here, and passing a Stream is completely useless

If you already have Files (e.g. in a form input), it's alright to pass them as metadata too. However, if you would normally fetch each file from a server, or generate them dynamically, please try using a dedicated metadata endpoint or function, and transforming its response into an array of {name, size} objects, rather than doing all the requests or computations in advance just to get a Content-Length.

An object with a name but no input and no size (not even zero) will be interpreted as an empty folder and renamed accordingly. To properly specify emtpy files without an input, set the size explicitly to zero (0 or 0n).

This iterable of metadata can be passed as the metadata property of downloadZip's options, or, if you want to display the predicted size without actually creating the Zip file, to the predictLength function (not exposed in the worker script). Naturally, the metadata and actual data must match, and be provided in the same order! Otherwise, there could be inaccuracies in Zip64 lengths.

In the case of predictLength, you can even save the return value and pass it later to downloadZip as the length option, instead of repeating the metadata.

Filename encoding

(tl;dr: set buffersAreUTF8: true in the options argument)

In ZIP archives, the language encoding flag indicates that a filename is encoded in UTF-8. Some ZIP archive programs (e.g. build-in ZIP archive viewer in Windows) might not decode UTF-8 filenames correctly if this flag is off.

client-zip always encodes string filenames (including filenames extracted from URLs) as UTF-8 and sets this flag for the related entries. However, downloadZip's options include a buffersAreUTF8 setting, affecting filenames that you supply as an ArrayBuffer (or ArrayView).

By default (when buffersAreUTF8 is not set or undefined), each ArrayBuffer filename will be tested, and flagged only if it is valid UTF-8. It is a safe default, but a little inefficient because UTF-8 is the only thing you can get in most contexts anyway. So you may tell client-zip to skip the test by setting buffersAreUTF8: true ; ArrayBuffers will always be flagged as UTF-8 without checking.

If you happen to get your filenames from a dusty API reading from an antique filesystem with non-ASCII filenames encoded in some retro 8-bit encoding and you want to keep them that way in the ZIP archive, you may set buffersAreUTF8: false ; ArrayBuffer filenames will never be flagged as UTF-8. Please beware that the stored filenames will extract correctly only with a ZIP program using the same system encoding as the source.

Benchmarks

updated in may 2023

updated again in may 2023 (experiment 3)

I started this project because I wasn't impressed with what — at the time — appeared to be the only other ZIP library for browsers, JSZip. I later found other libraries, which I've included in the new benchmarks, and JSZip has improved dramatically (version 3.6 was 40 times slower vs. currently only 40% slower).

I requested Blob outputs from each lib, without compression. I measured the time until the blob was ready, on my M1 Pro. Sounds fair?

Experiemnt 1 consists of 4 files (total 539 MB) manually added to a file input from my local filesystem, so there is no latency and the ZIP format structural overhead is insignificant.

Experiemnt 2 is a set of 6214 small TGA files (total 119 MB). I tried to load them with a file input as before, but my browsers kept throwing errors while processing the large array of Files. So I had to switch to a different method, where the files are served over HTTP locally by nginx and fetched lazily. Unfortunately, that causes some atrocious latency across the board.

Experiemnt 3 is the same set of 6214 TGA files combined with very small PNG files for a total of 12 044 files (total 130 MB). This time, the files are fetched by a DownloadStream to minimize latency.

client-zip@2.4.3 [email protected] [email protected] [email protected] [email protected]
experiment 1 Safari 1.647 (σ=21) s 1.792 (σ=15) s 1.912 (σ=80) s 1.820 (σ=16) s 2.122 (σ=60) s
baseline: 1.653 s Chrome 2.480 (σ=41) s 1.601 (σ=4) s 4.251 (σ=53) s 4.268 (σ=44) s 3.921 (σ=15) s
experiment 2 Safari 2.173 (σ=11) s 2.157 (σ=23) s 3.158 (σ=17) s 1.794 (σ=13) s 2.631 (σ=27) s
baseline: 0.615 s Chrome 3.567 (σ=77) s 3.506 (σ=9) s 5.689 (σ=17) s 3.174 (σ=22) s 4.602 (σ=50) s
experiment 3 Safari 1.768 (σ=12) s 1.691 (σ=19) s 3.149 (σ=45) s 1.511 (σ=38) s 2.703 (σ=79) s
baseline: 0.892 s Chrome 4.604 (σ=79) s 3.972 (σ=85) s 7.507 (σ=261) s 3.812 (σ=80) s 6.297 (σ=35) s

The experiments were run 10 times (not counting a first run to let the JavaScript engine "warm up" and ensure the browser caches everything) for each lib and each dataset, with the dev tools closed (this is important, opening the dev tools has a noticeable impact on CPU and severe impact on HTTP latency). The numbers in the table are the mean time of the ten runs, with the standard deviation in parentheses.

For the baseline, I timed the zip -0 process in my UNIX shell. As advertised, fflate run just as fast — in Chrome, anyway, and when there is no overhead for HTTP (experiment 1). In the same test, client-zip beats everyone else in Safari.

Conflux does particularly well in the second and third experiments thanks to its internal use of ReadableStreams, which seem to run faster than async generators.

Zip.js workers were disabled because I didn't want to bother fixing the error I got from the library. Using workers on this task could only help by sacrificing lots of memory, anyway. But I suppose Zip.js really needs those workers to offset its disgraceful single-threaded performance.

It's interesting that Chrome performs so much worse than Safari with client-zip and conflux, the two libraries that rely on WHATWG Streams and (in my case) async iterables, whereas it shows better (and extremely consistent) runtimes with fflate, which uses synchronous code with callbacks, in experiment 1. Zip.js and JSZip used to be faster in Chrome than Safari, but clearly things have changed. Experiments 2 and 3 are really taxing for Chrome.

In a different experiment using Deno to avoid storing very large output files, memory usage for any amount of data remained constant or close enough. My tests maxed out at 36.1 MB of RAM while processing nearly 6 GB.

Now, comparing bundle size is clearly unfair because the others do a bunch of things that my library doesn't. Here you go anyway (sizes are shown in decimal kilobytes):

client-zip@2.4.5 [email protected] [email protected] [email protected] [email protected]
minified 6.3 kB 29.8 kB 163.2 kB 198.8 kB 94.9 kB
minified + gzipped 2.6 kB 11 kB 58 kB 56.6 kB 27.6 kB

The datasets I used in the new tests are not public domain, but nothing sensitive either ; I can send them if you ask.

Known Issues

  • MS Office documents must be stored using ZIP version 2.0 ; use client-zip^1 to generate those, you don't need client-zip^2 features for Office documents anyway.
  • client-zip cannot be bundled by SSR frameworks that expect it to run server-side too (workaround).
  • Firefox may kill a Service Worker that is still feeding a download (workaround).
  • Safari could not download from a Service Worker until version 15.4 (released 4 march 2022).

Roadmap

client-zip does not support compression, encryption, or any extra fields and attributes. It already meets the need that sparked its creation: combining many fetch responses into a one-click donwload for the end user.

New in version 2: it now generates Zip64 archives, which increases the limit on file size to 4 Exabytes (because of JavaScript numbers) and total size to 18 Zettabytes. New in version 2.2: archive size can be predicted and used as the response's Content-Length.

If you need a feature, you're very welcome to open an issue or submit a pull request.

extra fields

Should be straightforward to implement if needed. Maybe client-zip should allow extending by third-party code so those extra fields can be plug-ins instead of built into the library.

The UNIX permissions in external attributes (ignored by many readers, though) are hardcoded to 664, could be made configurable.

ZIP64

Done.

compression

Limited use case. If the user is going to extract the archive just after downloading anyway, it's a waste of CPU. Implementation should be relatively easy with the new CompressionStream API. Incompatible with content-length prediction.

encryption

AES and RSA encryption could have been implemented with WebCrypto. However, only the proprietary PKWARE utility supports strong encryption of file contents and metadata. Well-supported Zip encryption methods (even using AES) do not hide metadata, giving you questionable privacy. Therefore, this feature is no longer planned for client-zip.

performance improvements

The current implementation does a fair bit of ArrayBuffer copying and allocation, much of which can be avoided with brand new (and sadly not widely supported yet) browser APIs like TextEncoder.encodeInto, TextEncoderStream, BYOB Streams and TransformStreams.

CRC-32 computation is, and will certainly remain, by far the largest performance bottleneck in client-zip. Currently, it is implemented with a version of Sarwate's standard algorithm in JavaScript. My initial experiments have shown that a version of the slice-by-8 algorithm using SIMD instructions in WebAssembly can run a bit faster, but the previous (simpler) WASM implementation is now slower than pure JavaScript.

Notes

A note about dates

The old DOS date/time format used by ZIP files is an unspecified "local time". Therefore, to ensure the best results for the end user, client-zip will use the client's own timezone (not UTC or something decided by the author), resulting in a ZIP archive that varies across different clients. If you write integration tests that expect an exact binary content, make sure you set the machine running the tests to the same timezone as the one that generated the expected content.

How can I include folders in the archive ?

When the folder has contents, just include the folder hierarchy in its content's filenames (e.g. { name: "folder/file.ext", input } will implicitly create "folder/" and place "file.ext" in it). Empty folders can be specified as { name: "folder/" } (with no size, no input, and an optional lastModified property). Forward slashes even for Windows users !

Any input object that has no size and no input will be treated as a folder, and a trailing slash will be added to its filename when necessary. Conversely, any input object that has a size or input (even an empty string) will be treated as a file, and the trailing slash will be removed if present.

Usage of predictLength or the metadata option must be consistent with the actual input. For exampe, if { name: "file" } is passed as metadata, client-zip will think it's an empty folder named "file/". If you then pass { input: "", name: "file" } in the same order to downloadZip, it will store the contents as an empty file with no trailing slash ; therefore, the predicted length will be off by at least one.

client-zip's People

Contributors

attez avatar bluetech avatar dependabot[bot] avatar headfox avatar horejsek avatar ikreymer avatar touffy avatar zakstucke avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

client-zip's Issues

[QUESTION] How to handle Download cancellations with Service Worker

Hi,
I use client-zip to download multiple large files over a service worker like in the worker.js example.

I just wondered if it is possible to propagate a cancel download event of the archive file to the fetch requests in the service worker so they don't continue running after the archive got cancelled.

I just found this issue: w3c/ServiceWorker#1544 and i'm wondering if it is possible at all in todays browsers regarding your latest Output stream cancellation commit #58.

Read a zip

Hello
Could you add a way to read a zip file? Jszip is pretty slow for that too

[BUG] Unzipped files have no permission on Linux

Describe the bug
When unzipping a ZIP file created with client-zip (non-service worker version), the resulting zipped files have no permissions in Linux and are therefore locked. Once you change the permissions, the files work as expected.

I tried using a File object and an object literal to represent each file and had the same outcome.

To Reproduce
Steps to reproduce the behavior:

  1. In a Linux environment, create a ZIP file using client-zip
  2. Extract the files
  3. Extracted files should have no permissions and possibly display a locked icon over them

Expected behavior
I expect the extracted files to have 664 permissions on Linux. This works as expected on Mac and Windows.

Screenshots
image

Desktop (please complete the following information):

  • OS: Pop!_OS 20.10 (Ubuntu variant)
  • Browser: Chromium
  • Version: 89

[FEATURE] Unicode Path extra field

Some old zip utilities do not understand UTF-8 in the basic filename field. They require the "Unicode Path Extra Field" instead.

That field could be included automatically whenever the original filename contains non-ASCII characters, while the basic field would be normalized to ASCII.

A solution along those lines was submitted in #22 . However, the cost in terms of extra code in the library and extra bytes in the generated archive was deemed too high compared to the gain (compatibility with a single outdated Zip utility identified so far). client-zip itself requires a very recent Web browser, so it's unlikely that it will run on a system that has an irremediably obsolete Zip program.

Please post in this thread if you run into the issue, and specify which Zip program is causing it.

SyntaxError: Unexpected token 'export'

Describe the bug
I get the following error when trying to import the package like so import { downloadZip } from "client-zip";:

SyntaxError: Unexpected token 'export'
    at compileFunction (<anonymous>)
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1031:15)
    at Module._compile (node:internal/modules/cjs/loader:1065:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:94:18)
    at Object.client-zip (/Users/ericamshukov/Projects/dt_bridge_pipetrekker_spa/.next/server/pages/index.js:20570:18) {
  page: '/'
}

To Reproduce
Steps to reproduce the behavior:

  1. Import in TypeScript file
  2. npm run
  3. SyntaxError: Unexpected token 'export'

Additional context
I am developing a project in NextJS and TypeScript, but everything else is working properly.

[BUG] Extracting duplicate files

Describe the bug
We have a Windows user that is reporting duplicate files after unzipping the folder. Does client-zip tested on Windows OS?

To Reproduce
Steps to reproduce the behavior:

  1. Concat multiple File using await downloadZip([file]).blob()
  2. Unzip file on Windows machine.

Unzipping the same files works on Mac OS.

Expected behavior
Should unzip with one file or number of files provided to downloadZip

Desktop (please complete the following information):

  • OS: Windows
  • Browser: Chrome
  • Version:

[FEATURE] Document CSP Policy required to use Client-Zip

Firstly great lib!

Just downloaded the latest version of client-Zip, and using Chrome 100.

Is your feature request related to a problem? Please describe.
I can only use the code on local host. When hosted, the problem is the code is blocked by the latest version of Chrome.
Researching the problem I added the following to the Content-Secutiry-Policy header:

script-src 'self' 'unsafe-inline' 'wasm-eval'; object-src 'self';

I can see in the headers of the network tab in Chrome the header has been set corretly, however I still get:

Uncaught CompileError: WebAssembly.Module(): Wasm code generation disallowed by embedder
    at client-zip.min.js:1:1840

Why is your feature relevant to client-zip?
Many people intend to use this lib with Chrome

Describe the solution you'd like
Some documented step in the already grear README about the CSP policy required to use client-zip.

Additional context
Thanks!

NOTE: This works but feels a bit over the top:

script-src 'self' 'unsafe-inline' 'wasm-eval' 'unsafe-eval'; object-src 'self';

Firefox closes service worker (how to know if a zip stream is still ongoing?)

I use client-zip in a vue application and I use it as a service worker. The service worker works perfectly on all browsers, but when a a single file (that would be packed inside the zip) takes more than 30 seconds to download, Firefox stops the entire service worker (as described here). This causes the file download to be marked as completed, while in reality, it's not.

To reproduce, set up a client-zip service worker and download a file that'll take more than 30 seconds to download on Firefox. It will be cut off, leaving a corrupt zip file.

My solution would be to use something like event.waitUntil() (https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil) on the fetch promise, but due to the nature of the fetch request (and streaming), this promise resolves as soon as the first data is received. I also don't know any other way to find out if a fetch request is actually finished that's useable in this scenario.

A (quite hacky) solution could also be to send a keep-alive type of fetch every ~15 seconds. Firefox recognizes a new request as the worker being "active", whilst an ongoing request is not. My application uses a lot of large files (sometimes even more than a gigabyte) which shouldn't be expected to be downloaded less than 30 seconds.

Is there any way to know wether a fetch download or a whole zip streaming is still active? Has anyone ever had this issue, and how have you dealt with it?

[BUG] Any files added to an archive after >4GB of data has already been added will be corrupted.

Describe the bug
Any files added to an archive after >4GB of file has already been added will be corrupted. A single file zipped that is greater than >4GB will not be corrupted, however.

To Reproduce
Steps to reproduce the behavior:

  1. Go to 'https://touffy.me/client-zip/demo/worker'
  2. Add a large file (I tested with a 6.5GB file), and another small file.
  3. Download the zip
  4. Run unzip CLI tool on Zip (also fails with Archive.app)
  5. See error

Expected behavior
Both files should be extracted from the zip successfully

Actual behavior
The first file is able to be extracted, but the next file fails:

~/Downloads λ unzip demozip.zip 
Archive:  demozip.zip
replace Archive.zip? [y]es, [n]o, [A]ll, [N]one, [r]ename: A
 extracting: Archive.zip             
file #2:  bad zipfile offset (local header sig):  1051

Desktop (please complete the following information):

  • OS: MacOS Monterey
  • Browser: Chrome
  • Version: 99.0.4844.84

Thank you for this library!

[FEATURE] Version without WebAssembly

Is your feature request related to a problem? Please describe.
Now client-zip is using crc32 implementation with WebAssembly which needs to allow unsafe evals in CSP headers.

Why is your feature relevant to client-zip?
client-zip is using WebAssembly. :-)

Describe the solution you'd like
There could be an option to avoid WebAssembly and use to vanilla JavaScript implementation.

Describe alternatives you've considered
It could automatically fall back to the non-WebAssembly implementation when an exception is thrown.

Additional context
I can create a pull request. Would you be interested in such a feature?

[FEATURE] Support ZIP64

As discussed on the roadmap and in #12, ZIP64 support is a highly desirable improvement for client-zip as it allows the creation of ZIP files larger than 4GB and containing files larger than 4GB themselves.

Once implemented, ZIP64 output would be the default for client-zip. That is consistent with other ZIP libraries, and the overhead is negligible. Making it the default is a slightly breaking change, but ZIP64 justifies a major version bump anyway.

Update 1:

While BigInts themselves — flawed though they may be — are finally available in browsers, the BigInt64 ArrayBuffer features have not yet landed in all browsers. But they can be polyfilled quite easily (actually I only need one: DataView.setBigUint64).

So that's what I'm going to do. The plan is to release client-zip 2.0 soon, with ZIP64. However, it will only work in browsers that support BigInts.

Update 2:

I just got the wrong byte order for the Zip64 extra field signature…

Update 3:

After some hexdumping and reverse-engineering of Zip64 files, I got client-zip to make valid ones too (or at least, files that 7zip and unzip don't complain about).

[BUG] file does not appear in the ZIP

Describe the bug
When I zip file that has name with upper case letters - it won't work.

To Reproduce
Steps to reproduce the behavior:

const file= new File( [ imageBlob ], 'something.JPG' );
const blob = await downloadZip( [ ...someotherfiles, file ] ).blob();
FileSaver.saveAs( blob, 'myFile.zip' );

The JPG file is missing

Expected behavior
The jpg file should be in the zipped file 'myFile.zip' .

If I try with lower-case 'something.jpg', then it works

MacOS Date Modified is incorrect and years in the future or the past

Thanks for the great package! It's lightning fast and super reliable.

We have installed in our app and during QC and noticed the following issue. Date Modified is showing some wild dates:

image

I have attempted to set the lastModified property with new Date() but it still has issues:

image

[BUG] zero-length files

"predictLength" fails to predict anything with zero length - file, string, array etc. It complains, that size is missing for that item.
On the other hand, "downloadZip" has no problem with those items.

[Help Wanted] The zip downloaded using ServiceWorker is empty

Hello,
Thanks for the amazing library!

I am currently using your awesome library to download multiple files from URLs to a zip file. After reviewing the demo and related instructions you provided, I believe using Service Worker is more suitable for my needs.

I built an HTTPS site using IIS on my machine and added your sample code, but it doesn't seem to work.
When I clicked the download button, the browser only downloaded an empty zip file. When I checked at the network tab, I noticed that the browser was still downloading files, so I'm sorry to bother you and see if you could provide me with some help, such as the possible reasons and possible solutions to this issue.

My code is no different from the sample code (demo/worker. html), but I downloaded the client-zip/worker.js and dl-stream/worker.js to my local machine.
image

Here is the GIF:
client-zip-sample

I am not sure if I have described it clearly. If there is any ambiguity in my expression, please let me know.

Thank you very much for checking my question.

[BUG] downloadZip is not a function when running the build script

Describe the bug
The unmodified worker demo doesn't start the download, it only shows downloadZip is not a function error when trying to download.

The issue is that the worker.js produced by the build script is an ES-module, and needs hand-editing to turn it into an IIFE.

To Reproduce
Steps to reproduce the behavior:

  1. Clone the repo (I'm on commit c740db5)
  2. Run npm run start
  3. Start a web-server at the root dir of the repo (in my case python3 -m http.server 8000)
  4. Open http://localhost:8000/demo/worker.html in your browser
  5. Add https://raw.githubusercontent.com/Touffy/client-zip/master/CHANGES.md to the list of URLs
  6. Click Download all that in one ZIP
  7. Observe the downloadZip is not a function

Expected behavior
Start the download (I haven't actually seen how the demo should work)

Workaround
Instead of compiling the scripts yourself, use a prebuilt worker.js from npm, GitHub Releases or remove module wrapper manually.

Desktop (please complete the following information):

  • OS: Pop!_OS 22.04 LTS
  • Browser: Chromium 105 with and without incognito mode. No extensions installed
  • Browser: Firefox 104

Additional context
npm --version 8.15
node --version v16.17.0
python3 --version 3.10.4

[BUG] testing with string input leads to uncompressed zip files

I am getting valid zip files, but the data is not compressed.

This is my first attempt to use a web worker and it has been much harder than I expected. I still haven't managed to setup CORS on S3 yet so that I can bundle files stored there, but in the meantime I've been trying to get everything else in place.

I'm using svelte-kit, and talking to the worker within the onMount() lifecycle function so that the browser is available.

onMount(async () => {
  await navigator.serviceWorker.register(
    `${$page.url.protocol}//${$page.url.host}/clientZipWorker.js`
  );
});

async function downloadAll(_e: Event) {
  let zipFileName = `bundle.${content.id}.zip`;
  let dateNow = new Date();
  const data = content.files.map((f) => {
    return {
      name: f.name,
      updated_at: f.updated_at,
      file_url: f.file_url,
      size_bytes: f.size_bytes,
    };
  });
  let keepAlive = setInterval(fetch, 4000, 'clientZipWorker/keep-alive', {
    method: 'POST',
  });
  const response = await fetch(`clientZipWorker/${zipFileName}`, {
    method: 'POST',
    body: JSON.stringify(data),
  });
  if (response.ok) {
    let url = URL.createObjectURL(await response.blob());
    downloadURL(url, zipFileName);
    URL.revokeObjectURL(url);
  }
  clearInterval(keepAlive);
}

My worker looks like this:

// I have pasted all content from https://unpkg.com/[email protected]/worker.js here
// 
//  var downloadZip=(()=>{"stream"in Blob ... etc
//

function randomString(length) {
  let result = '';
  const characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const charactersLength = characters.length;
  let counter = 0;
  while (counter < length) {
    let r = characters.charAt(Math.floor(Math.random() * charactersLength));
    result = result + r + r + r; // easy to compress
    counter += 3;
  }
  return result;
}

let DEBUG_FILE_LENGTH = 100000;

async function* mockActivateFiles(files) {
  for (const f of files) {
    console.log('mock input for: ', f.name);
    yield {
      name: f.name,
      lastModified: f.lastModified,
      input: randomString(DEBUG_FILE_LENGTH),
    };
  }
}

// must be sync
function* mockActivateMetadata(metadata) {
  for (const m of metadata) {
    console.log('mock metadata for: ', m.name);
    yield {
      name: m.name,
      size: DEBUG_FILE_LENGTH,
    };
  }
}

self.addEventListener('activate', () => {
  console.log('worker activated');
  clients.claim();
});

self.addEventListener('fetch', async (event) => {
  const url = new URL(event.request.url);
  const [, name] = url.pathname.match(/\/clientZipWorker\/(.+)/i) || [,];
  
  if (url.origin === self.origin && name) {
    console.log('worker matched fetch event:', name);
  
    event.respondWith(
      event.request
        .json()
        .then((data) => {
          const files = data.map((d) => {
            return {
              name: d.name,
              lastModified: d.updated_at,
              file_url: d.file_url,
            };
          });
          const metadata = data.map((d) => {
            return {
              name: d.name,
              size: d.size_bytes,
            };
          });
          return downloadZip(mockActivateFiles(files), {
            metadata: mockActivateMetadata(metadata),
          });
        })
        .catch((err) => {
          return new Response(err.message, { status: 500 });
        })
    );
  }
});

[BUG] Firefox downloads ends up being corrupt most of the time

Describe the bug
We've been using client-zip on our platform , we decided to not go for the streaming alternative initially since we wanted to support Safari as well. That is now fixed in recent Safari version, so we decided to use streaming instead.

To Reproduce
Unable to give reproduction steps unfortunately. As our website only support this for Chrome, Safari and Edge right now. But you can try out working version by using Chrome, Safari or Edge on bimobject.com (you have to register) and click download and making sure you select multiple files.

Expected behavior
Same as Chrome, Safari and Edge.

Screenshots

Archive downloaded through Firefox gets corrupt (most of the time)

Desktop (please complete the following information):

  • Mac, Linux and Windows
  • Firefox
  • Version 104.0 (64bit)

Additional context

The user can select files from a list that they want to download. It always works in Chrome, Safari and Edge. But in Firefox, the ZIP sometimes ends up complete (~10% of cases).

I ran zipdump

python3 zipdump.py ~/Downloads/firefoxdownload.zip

and it lists only some of the files

00000000: PK.0304: 002d 0008 0000 54af3988 00000000 00000000 00000000 0038 0000 |  0000001e 00000056 00000056 00000056 - Artificial ZZ plant 1100mmArtificial ZZ plant 1100mm.rfa
002c8056: PK.0708: a106776f 002c8000 002c8000 |  002c8066
002c8066: PK.0304: 002d 0008 0000 54af3986 00000000 00000000 00000000 0038 0000 |  002c8084 002c80bc 002c80bc 002c80bc - Artificial ZZ plant 1100mmArtificial ZZ plant 1100mm.dwg
005b5c55: PK.0708: c28b6444 002edb99 002edb99 |  005b5c65
005b5c65: PK.0304: 002d 0008 0000 54af398b 00000000 00000000 00000000 0038 0000 |  005b5c83 005b5cbb 005b5cbb 005b5cbb - Artificial ZZ plant 1100mmArtificial ZZ plant 1100mm.ifc

Same set of files on a chrome download

00000000: PK.0304: 002d 0008 0000 54af3988 00000000 00000000 00000000 0039 0000 |  0000001e 00000057 00000057 00000057 - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.rfa
002c8057: PK.0708: a106776f 002c8000 002c8000 |  002c8067
002c8067: PK.0304: 002d 0008 0000 54af3986 00000000 00000000 00000000 0039 0000 |  002c8085 002c80be 002c80be 002c80be - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.dwg
005b5c57: PK.0708: c28b6444 002edb99 002edb99 |  005b5c67
005b5c67: PK.0304: 002d 0008 0000 54af398b 00000000 00000000 00000000 0039 0000 |  005b5c85 005b5cbe 005b5cbe 005b5cbe - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.ifc
01287ad4: PK.0708: e6ed0337 00cd1e16 00cd1e16 |  01287ae4
01287ae4: PK.0304: 002d 0008 0000 54af3987 00000000 00000000 00000000 0039 0000 |  01287b02 01287b3b 01287b3b 01287b3b - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.obj
02c4f6e1: PK.0708: bffce8ef 019c7ba6 019c7ba6 |  02c4f6f1
02c4f6f1: PK.0304: 002d 0008 0000 54af3989 00000000 00000000 00000000 0039 0000 |  02c4f70f 02c4f748 02c4f748 02c4f748 - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.skp
04f9f516: PK.0708: 7160b309 0234fdce 0234fdce |  04f9f526
04f9f526: PK.0102: 032d 002d 0008 0000 54af3988 a106776f 002c8000 002c8000 0039 0000 0000 0000 0000 81b40000 00000000 |  04f9f554 04f9f58d 04f9f58d 04f9f58d - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.rfa
04f9f58d: PK.0102: 032d 002d 0008 0000 54af3986 c28b6444 002edb99 002edb99 0039 0000 0000 0000 0000 81b40000 002c8067 |  04f9f5bb 04f9f5f4 04f9f5f4 04f9f5f4 - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.dwg
04f9f5f4: PK.0102: 032d 002d 0008 0000 54af398b e6ed0337 00cd1e16 00cd1e16 0039 0000 0000 0000 0000 81b40000 005b5c67 |  04f9f622 04f9f65b 04f9f65b 04f9f65b - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.ifc
04f9f65b: PK.0102: 032d 002d 0008 0000 54af3987 bffce8ef 019c7ba6 019c7ba6 0039 0000 0000 0000 0000 81b40000 01287ae4 |  04f9f689 04f9f6c2 04f9f6c2 04f9f6c2 - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.obj
04f9f6c2: PK.0102: 032d 002d 0008 0000 54af3989 7160b309 0234fdce 0234fdce 0039 0000 0000 0000 0000 81b40000 02c4f6f1 |  04f9f6f0 04f9f729 04f9f729 04f9f729 - Artificial ZZ plant 1100mm/Artificial ZZ plant 1100mm.skp
04f9f729: PK.0506: 0000 0000 0005 0005 00000203 04f9f526 0000 |  04f9f73f 04f9f73f 

When trying to open the file on Ubuntu, it simply says: An error occurred loading the archive.

My service worker looks like this:

importScripts('./client-zip-2.2.2.min.js')

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url)

  if (url.pathname.includes('downloadZip/')) {
    event.respondWith(
      event.request
        .formData()
        .then(formData => downloadZip(activate(formData)))
        .catch(err => new Response(err.message, { status: 500 })),
    )
  }
})

function replaceHeader(response, productName) {
  const header = response.headers.get('Content-Disposition')
  productName = productName.replace(
    /[^\x00-\x7f]|[#|%|&|{|}|\\|\/|<|>|\*|\?|\$|!|'|"|:|@|\+|`|=]/g,
    '',
  )
  const parts = header.split(';')
  let fileName =
    productName +
    '/' +
    parts[1]
      .split('=')[1]
      .replace(/[^\x00-\x7f]|[#|%|&|{|}|\\|\/|<|>|\*|\?|\$|!|'|"|:|@|\+|`|=]/g, '')
  try {
    decodeURIComponent(fileName)
  } catch (ex) {
    fileName = 'unknown_filename'
  }

  var newHeaders = new Headers(response.headers)
  newHeaders.set('Content-Disposition', 'attachment; filename=' + fileName)
  const newFile = new Response(response.body, {
    headers: newHeaders,
  })
  return newFile
}
async function* activate(formData) {
  for (const value of formData.values()) {
    try {
      const parsedValue = JSON.parse(value)
      const response = await fetch(parsedValue.url)
      if (!response.ok) {
        console.warn(`skipping ${response.status} response for ${url}`)
      } else if (
        response.status === 204 ||
        response.headers.get('Content-Length') === '0' ||
        !response.body
      ) {
        console.warn(`skipping empty response for ${url}`)
      } else {
        yield replaceHeader(response.clone(), parsedValue.productName)
      }
    } catch (err) {
      console.error(err)
    }
  }
}

// Has to come after otherwise the download fetch is never executed. Needs to have try catch because it's not available on local development
try {
  importScripts('ngsw-worker.js')
} catch (e) {
  console.log('ngsw-worker.js not available (on local dev environment)', e)
}

and it's triggered with this Angular code

const form = document.createElement('form')
    form.method = 'post'
    form.action = `downloadZip/${downloadName}.zip`
    const button = document.createElement('button')
    button.type = 'submit'
    for (let file of fileDownloads) {
      const input = document.createElement('input')
      input.value = JSON.stringify({
        url: file.url,
        productName: file.productName,
      })
      input.name = 'url'
      input.type = 'hidden'

      form.appendChild(input)
    }

    form.appendChild(button)
    document.body.appendChild(form)
    form.submit()
    form.remove()
 

[FEATURE] Specify attachment filename

Is your feature request related to a problem? Please describe.
It would be useful to be able to set a custom attachment filename through the Content-Disposition header.

Why is your feature relevant to client-zip?
Makes it easier for downloading zip files with a custom filename.

Describe the solution you'd like
Can add filename to the options and then in downloadZip and then its a one-line change:

  const headers: Record<string, any> = { "Content-Type": "application/zip", "Content-Disposition": options.filename ? `attachment; filename="${options.filename}"` : "attachment" }

Describe alternatives you've considered
It's possible to work around this currently by creating a new response, but is a bit more cumbersome to do:

    let response = downloadZip(zip);
    const headers = {
      "Content-Disposition": `attachment; filename="${filename}"`,
      "Content-Type": "application/zip"
    };

    response = new Response(response.body, {headers});
    return response;

[BUG] Type error with string as input in downloadZip

Describe the bug
The type definitions in version >= 2.2.0 seem to exclude string as an input.

Is just InputWithSizeMeta missing as an optional input type here?
https://github.com/Touffy/client-zip/blob/v2.2.0/index.d.ts#L32

To Reproduce

const blob = await downloadZip([
  { name: "config.json", lastModified: now, input: JSON.stringify(config, null, 2) },
]).blob();

Results in following error:

Argument of type '{ name: string; lastModified: Date; input: string; }[]' is not assignable to parameter of type 'ForAwaitable<InputWithMeta | InputWithoutMeta>'.
  Type '{ name: string; lastModified: Date; input: string; }[]' is not assignable to type 'Iterable<InputWithMeta | InputWithoutMeta>'.
    The types returned by '[Symbol.iterator]().next(...)' are incompatible between these types.
      Type 'IteratorResult<{ name: string; lastModified: Date; input: string; }, any>' is not assignable to type 'IteratorResult<InputWithMeta | InputWithoutMeta, any>'.
        Type 'IteratorYieldResult<{ name: string; lastModified: Date; input: string; }>' is not assignable to type 'IteratorResult<InputWithMeta | InputWithoutMeta, any>'.
          Type '{ name: string; lastModified: Date; input: string; }' is not assignable to type 'InputWithMeta | InputWithoutMeta'.
            Types of property 'input' are incompatible.
              Type 'string' is not assignable to type 'Response | File | StreamLike | undefined'.

[QUESTION] How to stream zip without first loading all of it in memory

First of, thank you for this lib. I found this while browsing a zip64 feature request for the Conflux project.

Anyway I have a large number of ~100 Mb files lets say a total of 40 Gb that i would like to stream to the client as a zip, but i can´t figure out how to achieve that. i have read some of your closed issues to try to get an idea but i cant seem to get it to work.
This is kind of what i got but i tried a couple of different inputs to the generate function your DownloadStream for example.

const downloadAllClientZip = async () => {
  const files = await getFiles(); // load some file objects from a rest api

  const blob = await downloadZip(generate(files)).blob();

  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = `${params[0]}.zip`;
  link.click();
  URL.revokeObjectURL(link.href);
  link.remove();
};

async function* generate(files) {
  for (const { href, folder, title } of files)
    yield {
      input: await fetch(href),
      name: `${folder}/${title}`,
    };
}

🙇

[BUG] Non-ASCII characters in file names come out as garbage in Windows Explorer

Describe the bug
Special characters (äöüè ...) in the file name are not displayed correctly with Windows Explorer. Even if the file is unzipped by the Windows own tool the filename is not displayed correctly. However, unpacking by 7zip works correctly.

If the ZIP file is created with JSZIP, the file names are displayed correctly with the Windows tools.

Example:

  • expected filename: äöüè___milano.gif
  • displayed filename: ├ñ├Â├╝├¿___milano.gif

To Reproduce


async function downloadTestZip() {
  // define what we want in the ZIP
  const intro = { name: "äöüè_milano.txt", lastModified: new Date(), input: "Hello. This is the client-zip library." }

  // get the ZIP stream in a Blob
  const blob = await downloadZip([intro]).blob()

  // make and click a temporary link to download the Blob
  const link = document.createElement("a")
  link.href = URL.createObjectURL(blob)
  link.download = "test.zip"
  link.click()
  link.remove()

  // in real life, don't forget to revoke your Blob URLs if you use them
}

Expected behavior
The filename should be created with the same character set as with the other tools (example JSZIP).

Desktop (please complete the following information):

  • Windows 11
  • Browser: Chrome Version 109.0.5414.75 (Official Build) (32-bit)

Re-evaluate slice-by-8 or slice-by-16 CRC32

Hello! I've been working on a ZIP implementation in JavaScript myself, fflate. While trying to optimize CRC32 I devised a very fast, pure JS implementation using slice-by-16 CRC32. From my testing this is consistently faster than your current WASM implementation by 20-30%. I noticed you mentioned in the README that slice-by-16 didn't yield meaningful improvements, but this seems to suggest otherwise. You're free to use the JS code if you'd like, but maybe a WASM CRC32 implementation with Slice-by-16 could be faster? I'd be happy to help with implementing my CRC32 code or developing a fast WASM CRC32 implementation if you're interested. Thanks for maintaining this library!

How to download multiple pdfs in a zip?

Hi! Thanks for creating this awesome library
I'm struggling with replacing JSZip with your library
I need to download several files in an array like this

exampleFiles = [
    {
        name: "1.pdf",
        url: "/storage/files/1.pdf'
    },
    {
        name: "hello.pdf",
        url: "/storage/files/hello.pdf'
    },
    {
        name: "abcd.pdf",
        url: "/storage/files/abcd.pdf'
    },
]

// this function is a layer of abstraction to download files
// it uses JSZip to fetch the remote files and download them 
// in a zip archive. More important, it works well!
downloadFilesInZip(files)

Can you help me implement this using your library? I've tried to do it following the code provided in the README, but it seems like the fetch api gets the binary content of the pdf and it is saved as plain text; therefore I can't display the file after downloading the zip

Thanks in advance

[BUG] non-english file names

Files with non-english names, e.g. hebrew or russian, are packed into archive so, that Windows ZIP shell extension sees them as gibberish.
On the other hand, 7zip sees them correctly.

[BUG] Microsoft Office can not read generated ZIP containing ODS

Describe the bug

ODS generated with client-zip is invalid in Microsoft Office

To Reproduce
Steps to reproduce the behavior:

  1. Go to https://sheetjs.com/demo/table
  2. Click on 'Export to ODS'
  3. The generated ODS is a ZIP which works in Microsoft Office
  4. If you generate the same ZIP with client-zip, Microsoft Office rejects it

Investigations
Comparing various ODS that Microsoft Office accepts, Microsoft Office requires a ZIP "version needed to extract" 2.0 instead of 4.5

Possible workaround
Lower the ZIP version number makes it work:

-  header.setUint32(4, 0x2d_00_0800) // ZIP version 4.5 | flags, bit 3 on = size and CRCs will be zero
+  header.setUint32(4, 0x14_00_0800) // ZIP version 2.0 | flags, bit 3 on = size and CRCs will be zero

NB: I edited the issue because I thought I made it work with version 4.5 with different "META-INF/" entry but it seems it was a wrong test...

[FEATURE] Propagate cancellation

Is your feature request related to a problem? Please describe.
Currently I'm not aware of any mechanism to thread cancellation through to the file streams.

Why is your feature relevant to client-zip?
It would be nice to be able to abort outbound requests if the zip download is cancelled by the user.

Describe the solution you'd like
Some mechanism to propagate cancellation. Maybe it would work to thread a handle to all the streams that are cancellable through to the ReadableFromIter stream here, and then cancel them in the cancel callback of the ReadableStream constructur or something similar.

Describe alternatives you've considered
I suppose it might work if I maintain my own handle to all the streams, use makeZip instead of downloadZip, and build my own Response in my service worker.

[FEATURE] Store paths as well as names

From what I understand (please correct me if I'm wrong) it is currently only possible for client-zip to generate a "flat" zip file. I.e. no folder paths in the zip file. Would it be very difficult to add this?

Why is your feature relevant to client-zip?
There are definitely a use cases where this would be useful. On top of my head.

  • Avoids having to mangle the filename itself if there are duplicate filenames in the files/requests to be stored in the zip. (Just use different folders)
  • Keep folder structure if user needs to download files from a system that actually stores files in folders (plenty). Duplicate filenames are often not that rare in those cases.

Describe the solution you'd like
Make it possible to not only specify name for a entry in the zip package but also folder path (relative is probably enough).

[FEATURE] Announce total length in HTTP header 'Content-Length'

Thank you for your library. I was wondering if it is possible to calculate the download progress as a percentage. To do this, the response object of downloadZip() would have to contain the HTTP header Content-Length, which it does not at the moment.

I have already found that browsers hide HTTP headers from scripts on cross-origin requests (see here) unless the HTTP header Access-Control-Expose-Headers says otherwise. So, I adapted my server config to expose the Content-Length header. As a result, the request to fetch my test file returns the HTTP header Content-Length (see (1) in code snippet). But, because the HTTP header Content-Length is still missing on the response of downloadZip() (see (2) in code snippet), I cannot calculate a progress in percent (as the absolute value is unknown).

So my question is: is it even possible to include the HTTP header Content-Length and have it contain the sum of all content lengths of the files to be downloaded?

The following code snippet demonstrates how to observe the download progress:

import { downloadZip } from 'https://cdn.jsdelivr.net/npm/client-zip/index.js';

async function download() {

    /// (1) Original HTTP request where the Content-Length header is present
    const file = await fetch('https://4sc35swexhjkk3fe-public.s3-eu-west-1.amazonaws.com/elevator-music.mp3');

    const response = await downloadZip([file]);
    const reader = response.body.getReader();

    /// (2) Content-Length header is missing -> always 0 -> cannot calculate a relative progress
    const contentLength = +response.headers.get('Content-Length');

    let receivedLength = 0;
    const chunks = [];
    while (true) {
        const { done, value } = await reader.read();

        if (done) {
            break;
        }

        chunks.push(value);
        receivedLength += value.length;

        console.log(`Received ${receivedLength} of ${contentLength}`);
    }

    const blob = new Blob(chunks);

    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'test.zip';
    link.click();
    link.remove();
}

download();

[FEATURE] Support parallel downloading

Is your feature request related to a problem? Please describe.
Not really a problem, more just a potential performance improvement

Why is your feature relevant to client-zip?
client-zip currently is not very amenable to doing parallel downloads for the files going into the zip. But parallellizing or batching the downloads can result in pretty significant improvements on the total download speed. You can try tricking it by doing something along these lines:

const mkFile = async (input: SomeType): Promise<InputWithoutMeta> => { ... };
...
downloadZip(activate(inputs));
...
async function* activate(inputs: SomeType[]) {
  const rest = await Promise.all(inputs.map(input => mkFile(input))); // or do this in batches instead
  for (const file of rest) {
    yield file;
  }
}

But that will cause the response to not come back to the browser until after all the files (or at least the first batch) are downloaded, so so it's a bad experience and it effectively breaks streaming.

So instead, something like this works:

downloadZip(raceAll(inputs.map(input => mkFile(input))));

async function* raceAll<T>(promises: Promise<T>[]): AsyncGenerator<T> {
  const wrappedPromises: Promise<T>[] = [];
  const resolvers: ((input: Promise<T>) => void)[] = [];

  for (const promise of promises) {
    wrappedPromises.push(new Promise(resolve => resolvers.push(resolve)));
    promise.finally(() => (resolvers.shift() as (input: Promise<T>) => void)(promise));
  }

  for (const promise of wrappedPromises) {
    yield await promise;
  }
}

(credit to https://stackoverflow.com/questions/70044213/async-generator-yielding-promise-results-as-they-are-resolved for helping me put together raceAll)

But there are some issues -- for one, this will still wait for the first file to be completed before initiating the file selection in the browser, which isn't a great experience and could be a really slow wait, depending on how slow the files are to download. For two, it's simply ugly and complicated for what seems like it would be a pretty common use case, and it's not exactly straightforward to write a raceAll generator that does the right thing (although I feel sure there are some libraries somewhere that have that function)

Describe the solution you'd like
I think the solution here would split into two separate changes:

  1. Find a way to trigger the download to start before the first file download completes. I'm not familiar enough with the ReadableStream apis to know how / if this is possible, but maybe it would be possible to enqueue an empty chunk to the controller in the start callback to the stream? Or maybe a fix here involves setting some magic combination of headers onto the Response, but I'm not sure if the response is even being received by the browser before the first file comes back (I haven't figured out how to see the responses from a service worker in web inspector yet). I would need to play with it a bit more to know what's possible here as it's a bit out of my expertise, but I am guessing there's some way to get the stream to be seen as opened by the browser even though no data has yet been received.
  2. Expose some nicer apis for doing things in batches. Ideas here that may help could be e.g. bundling raceAll and wrapping it in something like downloadZipBatch: (inputs: Promise<InputWithoutMeta>[], batchSize: number) => Response.

Describe alternatives you've considered
see above

[FEATURE] Encryption

Why is your feature relevant to client-zip?
Encryption is on client-zip roadmap, but the author does not see use case for it. Therefore, I would like to add my use case.

Describe the solution you'd like
I am running workers to backup data from cloud services to another, as zip archives. Certain users have requested for their backups to be encrypted, since they may contain personal data. Currently, I have to fetch data locally, zip it with encryption, and then upload again. It would be nice to be able to use client-zip to do it all in one step.

Describe alternatives you've considered
There is not really a JS library that does encryption for zip, so the alternative is really just to download data all at once, zip it with encryption, and upload the result.

[BUG] You may need an appropriate loader to handle this file type

Describe the bug
In my Nuxt project I'm using in component like and its giving the error
You may need an appropriate loader to handle this file type

To Reproduce
Steps to reproduce the behavior:
add import in vue component
import { downloadZip } from "client-zip";

any solution to this error

[BUG] downloadZip creating corrupted zips in demo

I am running this simple script from demo and its creating corrupted zip file:

function test() {
  // an ArrayBuffer
  const hi = new TextEncoder().encode("hello world!")
  // just a string
  const bye = "goodbye."

  const blob = downloadZip([
    { name: "hello.txt", lastModified: new Date(), input: hi },
    { name: "goodbye.txt", lastModified: new Date(), input: bye }
  ]).blob()

    const link = document.createElement("a")

    var binaryData = [];
    binaryData.push(blob);

    link.href = URL.createObjectURL(new Blob(binaryData, {type: "application/zip"}))
    link.download = "hello+goodbye.zip"
    link.click()
}

document.getElementsByTagName("button")[0].onclick = test

While I am opening hello+goodbye.zip with vim, I can only find a text:
[object Promise]
No file list in zip file and it also can not be opended in nautilus either.

[FEATURE] Generate ranged responses with `downloadZip`

Is your feature request related to a problem? Please describe.
I have found this only client library that generates zip in streaming fashion to use in cloudfare workers.
But the response doesn't seems to be truely streaming because of the lack of partial content responses

Why is your feature relevant to client-zip?
Since cloudfare Workers have browser based architecture ( having whatwg compliant streaming APIs like ReadableStream , TransformStream ) , so i needed a client library that is capable of generating zips in streaming fashion for large outputs

Describe the solution you'd like
At the moment , I'm not sure but seems like this function

client-zip/src/zip.ts

Lines 51 to 116 in a338414

export async function* loadFiles(files: ForAwaitable<ZipEntryDescription & Metadata>, options: Options) {
const centralRecord: Uint8Array[] = []
let offset = 0n
let fileCount = 0n
let archiveNeedsZip64 = false
// write files
for await (const file of files) {
const flags = flagNameUTF8(file, options.buffersAreUTF8)
yield fileHeader(file, flags)
yield file.encodedName
if (file.isFile) {
yield* fileData(file)
}
const bigFile = file.uncompressedSize! >= 0xffffffffn
const bigOffset = offset >= 0xffffffffn
// @ts-ignore
const zip64HeaderLength = (bigOffset * 12 | bigFile * 28) as Zip64FieldLength
yield dataDescriptor(file, bigFile)
centralRecord.push(centralHeader(file, offset, flags, zip64HeaderLength))
centralRecord.push(file.encodedName)
if (zip64HeaderLength) centralRecord.push(zip64ExtraField(file, offset, zip64HeaderLength))
if (bigFile) offset += 8n // because the data descriptor will have 64-bit sizes
fileCount++
offset += BigInt(fileHeaderLength + descriptorLength + file.encodedName.length) + file.uncompressedSize!
archiveNeedsZip64 ||= bigFile
}
// write central repository
let centralSize = 0n
for (const record of centralRecord) {
yield record
centralSize += BigInt(record.length)
}
if (archiveNeedsZip64 || offset >= 0xffffffffn) {
const endZip64 = makeBuffer(zip64endRecordLength + zip64endLocatorLength)
// 4.3.14 Zip64 end of central directory record
endZip64.setUint32(0, zip64endRecordSignature)
endZip64.setBigUint64(4, BigInt(zip64endRecordLength - 12), true)
endZip64.setUint32(12, 0x2d03_2d_00) // UNIX app version 4.5 | ZIP version 4.5
// leave 8 bytes at zero
endZip64.setBigUint64(24, fileCount, true)
endZip64.setBigUint64(32, fileCount, true)
endZip64.setBigUint64(40, centralSize, true)
endZip64.setBigUint64(48, offset, true)
// 4.3.15 Zip64 end of central directory locator
endZip64.setUint32(56, zip64endLocatorSignature)
// leave 4 bytes at zero
endZip64.setBigUint64(64, offset + centralSize, true)
endZip64.setUint32(72, 1, true)
yield makeUint8Array(endZip64)
}
const end = makeBuffer(endLength)
end.setUint32(0, endSignature)
// skip 4 useless bytes here
end.setUint16(8, clampInt16(fileCount), true)
end.setUint16(10, clampInt16(fileCount), true)
end.setUint32(12, clampInt32(centralSize), true)
end.setUint32(16, clampInt32(offset), true)
// leave comment length = zero (2 bytes)
yield makeUint8Array(end)
}

here it can accept byteRange property as 2 element array or start and end properties in options object to yield only those bytes whose index lies inbetween this range.

Describe alternatives you've considered
Currently it is possible to achieve same thing manually with makeZip output stream and wrapping around with own implemented function

Additional Context
Thanks for such library, expecting more features like a modular approach same as node-archiver
So the implementation can be platform , format agnostic

[Question]: Does client-zip work with streamsaver ?

Hello !

First of all, thank you for your work.
I'm not a pro but I couldn't manage to use client-zip with streamsaver ? I'm not sure if this is because it's not the purpose of your lib or if i'm wrong somewhere. If there is a way to do it, could you help me with a small code snippet please ?

Thanks

[QUESTION] Lazyfetch array with objects instead of urls

I've been using client-zip on deno to zip-on-the-fly some files on my origin server using the following code:

async function *lazyFetch() {
      for (const url of urls) yeild await fetch(url)
}

However with this way I cannot put files in different folders.

I would like to be able to have an array with an object for each file, for example:

let files = [
    {
        name: "folder1/file1.txt",
        size: 123,
        input: "https://file-download-url.com/file"
    }
]

and lazyfetch it. Is this doable?

Thank you.

[Question] Is it suitable for my use case

My users upload multiple files for retrieval later using S3.

*Up to 100 files (max 500) can be uploaded at one time
*Total Size average's from 200mb up to 6GB or more.
*Current use case is that when the clients want to download it they have the option to download individually, or i force the browser to download multiple files!

I have been looking for a way to either compress the files from s3 along with the key stream, zip and download.
OR create the zip client side and then upload.
I dont need to compress.

I wonder if client-zip would be suitable, the only current limitation would be the 4gb limit, but then i may be able to separate the zips.

Thanks
Ricky

Add canvas image to zip

I have canvas with image, converted to dataURL()

const img = document.getElementById("canvas_output").toDataURL();

const blob = await downloadZip([ img ]).blob();

this gives me error

[BUG] Breaks react-native build with obtuse error

Describe the bug
When using client-zip in a shared context, react-native fails to build.

We have both a web & mobile app that use a shared "core" folder.

myApp/
├─ clients/
│  ├─ core/
│  │  ├─ sharedFunction.ts
│  │  ├─ package.json
│  ├─ web-app/
│  │  ├─ app.tsx
│  │  ├─ package.json
│  ├─ react-native-app/
│  │  ├─ app.tsx
│  │  ├─ package.json

We use client-zip inside core for a util that is shared between web & mobile. This however breaks the mobile build with the following error:

Compiling JS failed: 6000907:22:invalid numeric literal Buffer size 3141939 starts with 766172205f5f42554e444c455f535441

Looking online nothing appears for "Compiling JS failed invalid numeric literal Buffer size starts with" other than this single stack-overflow question which involves handling promises incorrectly.

We fixed this issue by moving the function declaration to web-app & not allowing the feature to be used natively on mobile.

The function looked like this:

// ... Truncated
const zipStreamAsBlob = await downloadZip(
  blobs
    .filter((blob) => blob.file)
    .map((blob, i) => ({
      input: blob.file!,
      name: `${files[i].name}${files[i].uri.slice(
        files[i].uri.lastIndexOf('.'),
      )}`,
    })),
).blob();
// ... Truncated

Our work-around is good for now, we didn't exactly need the feature in mobile however there was nothing online about this issue so I believe its still worth posting.

Expected behavior
React native project should build

Screenshots
imgur

Desktop:

  • OS: macOS Monterey (12.4)

Smartphone:

  • Device: iPhone 13 Pro
  • OS: iOS 15.5

[Performance] use TextEncoderStream where it is supported

When the input for a ZIP entry is a string, it is currently transformed into a Uint8Array, even for large strings. When the browser supports TextEncoderStream , we should use that and set the entry's internal bytes to the resulting stream instead of a Uint8Array.

Uint8Array streams (the readable half of a TextEncoderStream) are already a valid internal type, so only input normalization is impacted by this change.

Sadly, that feature is not yet implemented in Safari, so it needs to be an optional code path with feature detection.

I don't expect this change to be massively useful in the long term (since using large strings as input is a bad idea anyway) but it should be simple.

Add a ServiceWorker demo

Creating a Blob is the only way to get a download when running client-zip directly in the browser window's JavaScript context. But that means the whole archive needs to be in memory and the download won't start until then.

It would be more efficient (particularly if you bundle lots of files (or large ones) from HTTP requests) to use a ServiceWorker to stream the archive. It also allows means there is no unseemly creating and clicking of a Blob URL link and subsequent revocation. Using local Files in the archive would be more difficult, though, and won't be included in the demo (it's also not included in the worker script distribution of the library).

The streaming ServiceWorker usage is only a little more complicated than the basic Blob usage, but it needs a demo so that more users of client-zip will take that step confidently.

[BUG] Blob is not defined error in SvelteKit project

Hi, I have seen the documentation for this awesome module.
I am running my SvelteKit project.
I have included downloadZip in my +page.ts file.
But then, if I run it, I got Blob is not defined error.
Please help!

[HELP] Adding a large number of files via array.

This is neither a bug nor a feature but this seemed the best place to ask.

I want to save the 900+ list items on a page as separate files in a zip. It's easy enough to itterate them and put them in an array/object (most of my experience is with php so I always struggle with this). But I can't see how I can pass this to this line of code:

wait downloadZip([intro, code]).blob()

Is there any way of doing this? The number of files will grow over time so I need a programmatic way of passing them all to the blob.

Thanks!

[HelpWanted] Download progress

Hello,
Thanks for the great library.

I am facing problem for displaying the download progress

here's my code:

   const files = response.data.files

    totalBytes = getTotalBytes(files)

    const requests = files.map((file) => fetch(file.url)
      .then(async (response) => {
        const reader = response.body.getReader()
        const chunks = []
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            break;
          }
          chunks.push(value)
          receivedBytes += value.length;
          progress(receivedBytes)
        }
        return new Blob(chunks) //--> dont know if this is the correct way
      })
      .catch(error => {
        throw new Error(error.message + "\nURL: " + file.url)
      })
    )
    const responses = await Promise.all(requests)
    
    console.log("response: ", responses) //--> this works and output like:  (11) [Blob, Blob, Blob, Blob, Blob, Blob, Blob, Blob, Blob, Blob, Blob]

    const blob = await downloadZip(responses); //--> not working

    const link = document.createElement("a")
    link.href = URL.createObjectURL(blob)
    link.download = fileName + ".zip"
    link.click()
    URL.revokeObjectURL(link.href)
    link.remove()

I've tried solution mentioned here: #19
but it does not calculate when files are downloaded but rather I think when making zip file

Any help on this matter Please

[FEATURE] Directories

Hi, client-zip is a great lib, thank you very much for your work.
It would be awesome to be able to organize files in sub-directories into the output zip (maybe it is already and I didn't found how?).
Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.