Angular 11 - To add files zip using JSZip

I am trying to add several txt files to a zip file and download it locally.

For this, I am using the libraries: JSZip and FileSaver.

This is the “typescript” code, of the download button:

let zip = new JSZip();
zip.file("readme.txt", "Files required");
let txtFile = zip.folder("txt");
this.selectedItems?.forEach((item.name) => {
this.downloadService
.downloadFile(item.name)
.subscribe((response) => {
  let base64 = response.output.split(",");
  txtFile.file(item.name, base64[1], {base64: true});
});
});
zip.generateAsync({type:"blob"})
.then((content) => {
// see FileSaver.js
FileSaver.saveAs(content, this.fileZipName);
});

This code, downloads the zip file successfully, with a readme.txt file and an empty txt folder. The txt folder should contain the txt files obtained from the “downloadService” service.

Of course, removing the call to the service that returns the files, and putting a hardcoded base 64 file, if we correctly generate the zip with its txt folder and the corresponding files inside it, with the same content in all the TXT files, because is hardcoded.

let zip = new JSZip();
zip.file("readme.txt", "Files required");
let txtFile = zip.folder("txt");
this.selectedItems?.forEach((item) => {
txtFile.file(item.name, "VGVzdCBBbGltXzEw", {base64: true});
});
zip.generateAsync({type:"blob"})
.then((content) => {
// see FileSaver.js
FileSaver.saveAs(content, this.fileZipName);
});

I update the question, with the code I’m testing at the moment, this generates several zips depending on the files processed, in the last zip is adding it the files to the txt folder. But I need to generate a single zip file. FileSaver.saveAs, being inside the loop that recovers the files, will generate a zip for each file, and in the last zip, if it adds all the files that I need.

let zip = new JSZip();
zip.file("readme.txt", "Files required");
let txtFile = zip.folder("txt");
this.selectedItems?.forEach((item) => {
this.downloadService.downloadFile(item.name)
  .subscribe((response) => {
    let base64 = response.output.split(",");
    txtFile.file(item.name, base64[1], {base64: true});
    zip.generateAsync({type:"blob"})
      .then((content) => {
      // see FileSaver.js
      FileSaver.saveAs(content, this.fileZipName);
    });
  });
});

selectedItems: It is an array of objects that contains the files, the name property contains the name of the file.

I don’t know how to call the “downloadFile” service so that for each file that exists, it makes the corresponding call, to obtain the base64 and add it to the zip.

Depending on the files that we select, if for example we have 2 files, the response of the service would be as follows:

Call one file:

{"errorMessages":[],"output": "data:text/plain;base64,VGVzdCBJbmZyYTEw"}

Call one file:

{"errorMessages":[],"output": "data:text/plain;base64,VGVzdCBJbmZyYTEy"}

The base64[1] variable contains the base64 code of the txt file.

In short, when I hardcode the base64 code it works fine and compresses the files with the same content, but if I try to get the base64 code dynamically by calling my “downloadFile” service, it doesn’t add them to the zip in the txt folder and doesn’t give any errors.

Thanks,

The issue you’re facing is that you are calling zip.generateAsync({ type: "blob" }) inside the loop that handles asynchronous service calls, and this is causing the zip file to be generated and saved multiple times, resulting in only the last file being included in the zip. The zip.generateAsync() call needs to occur only after all the files have been added to the zip, and not during each service call.

To resolve this issue, you need to:

  1. Ensure that all files are added to the zip object before generating and downloading the zip.
  2. Use Promise.all() to handle the asynchronous service calls, ensuring that you only generate the zip after all files have been processed.

Solution:

You can collect all the promises from the downloadFile service calls and wait for all of them to complete using Promise.all(). After all the files are added, you can then generate and download the zip.

Here’s how you can modify your code:

let zip = new JSZip();
zip.file("readme.txt", "Files required");
let txtFile = zip.folder("txt");

const filePromises = this.selectedItems?.map((item) => {
  return this.downloadService.downloadFile(item.name).toPromise()
    .then((response) => {
      let base64 = response.output.split(",");
      let base64Data = base64[1]; // Extract the actual base64 content
      txtFile.file(item.name, base64Data, { base64: true });
    });
});

// Wait for all files to be added before generating the zip
Promise.all(filePromises).then(() => {
  // Generate the zip file after all files have been added
  zip.generateAsync({ type: "blob" })
    .then((content) => {
      // Use FileSaver.js to trigger the download of the zip file
      FileSaver.saveAs(content, this.fileZipName);
    });
});

Key Changes:

  1. Promise Handling: Instead of calling zip.generateAsync() inside the loop, we collect all promises in the filePromises array. Each service call to downloadFile(item.name) is converted to a promise using .toPromise().
  2. Waiting for Completion: We use Promise.all(filePromises) to ensure that the zip.generateAsync() function is called only after all files have been added to the zip. This guarantees that all selected files will be included in the zip.
  3. Avoid Generating Zip Multiple Times: The call to zip.generateAsync({ type: "blob" }) is now only made once, after all files have been added to the zip, which prevents the zip from being generated multiple times during the loop.

Explanation:

  • this.selectedItems?.map(...) is used to iterate over the selected items and create a promise for each downloadFile service call.
  • Promise.all(filePromises) ensures that the zip is only generated after all files have been processed and added to the txt folder in the zip.
  • zip.generateAsync({ type: "blob" }) is now called only once, after all asynchronous calls are finished, ensuring the zip contains all files and is downloaded correctly.

Optional: Handling Errors

If you want to handle any potential errors (e.g., if a file download fails), you can modify the code to catch errors and handle them appropriately:

Promise.all(filePromises)
  .then(() => {
    zip.generateAsync({ type: "blob" })
      .then((content) => {
        FileSaver.saveAs(content, this.fileZipName);
      });
  })
  .catch((error) => {
    console.error("Error while downloading files or generating the zip:", error);
  });

This ensures that you can track and handle any errors during the file download process.