How to merge and unzip (.zip, .z01, .z02) in javascript (client side only)?

Viewed 542

I am trying to download the splitted zip file coming from api call in parts (.zip, .z01, .z02) etc. In Javascript I want to download these files on the fly and create a combined .zip file and then save the unzipped file. (i don't need to create a combined zip) I simply need test.pdf file which which has been splitted to multiple zip. I want to download that pdf file.

e.g. test.pdf (18 mb) is splitted into two zip files of (.zip 10mb) and (.z01 8mb). currently I am downloading them as blob and then merging them. the resulting zip is currupted. below is the code I tried.

Promise.all([
  axios.get(`https://testapi.com/folder/file.zip`, {
    responseType: "blob",
    headers: {
      Accept: "application/json",
    },
  }),
  axios.get(`https://testapi.com/folder/file.z01`, {
    responseType: "blob",
    headers: {
      Accept: "application/json",
    },
  }),
]).then((obj) => {
  let blob: any = [];
  obj.forEach((e: any) => {
    blob.push(e.data);
  });
  const newBlob = new Blob(blob, { type: "octet/stream" });
  saveAs(newBlob, "zip.zip"); // but it's currupted
});
2 Answers

You could use JSZip and FileSaver in order to achieve this. Note that the files are ordered alphabetically in the input so this snippet requires your files to be named accordingly. I have used an input in order to make testing easier, you could easily adapt it to axios requests for each file.

The snippet actually doesn't work on this web page because the downloads are disallowed by the snippet iframe's policy so you have to try it by other means.

const objectWithoutProperties = (obj, keys) => {
  const target = {};
  for (let i in obj) {
    if (keys.indexOf(i) >= 0) continue;
    if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
    target[i] = obj[i];
  }
  return target;
}

const mergeFiles = (a, b) => {
  if (!a) return b;
  const intersect = Object.keys(a.files).filter((x) =>
    Object.keys(b.files).includes(x)
  );

  a.files = { ...a.files, ...objectWithoutProperties(b.files, intersect) };

  return Promise.all(
    intersect.map((file) =>
      Promise.all([
        a.file(file).async("arraybuffer"),
        b.file(file).async("arraybuffer"),
      ])
        .then((x) => new Blob(x))
        .then((x) => a.file(file, x))
    )
  ).then(() => a);
};

const MultipleZipMerger = () => {
  function change(e) {
    Promise.all([...e.target.files].map(JSZip.loadAsync))
      .then((x) =>
        x.reduce(
          (acc, cur) => acc.then((x) => mergeFiles(x, cur)),
          Promise.resolve()
        )
      )
      .then((x) => x.generateAsync({ type: "blob" }))
      .then((x) => saveAs(x, "hello.zip"));
  }
  
  return <input onChange={change} type="file" multiple />;
};

ReactDOM.render(<MultipleZipMerger />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>

<div id="root"></div>

This is a standalone version of this code which you can test as a local file:

<!doctype html>

<html>
</head>

<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script>

<div id="root"></div>
<script>
  
const objectWithoutProperties = (obj, keys) => {
  const target = {};
  for (let i in obj) {
    if (keys.indexOf(i) >= 0) continue;
    if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
    target[i] = obj[i];
  }
  return target;
}

const mergeFiles = (a, b) => {
  if (!a) return b;
  const intersect = Object.keys(a.files).filter((x) =>
    Object.keys(b.files).includes(x)
  );

  a.files = { ...a.files, ...objectWithoutProperties(b.files, intersect) };

  return Promise.all(
    intersect.map((file) =>
      Promise.all([
        a.file(file).async("arraybuffer"),
        b.file(file).async("arraybuffer"),
      ])
        .then((x) => new Blob(x))
        .then((x) => a.file(file, x))
    )
  ).then(() => a);
};

const MultipleZipMerger = () => {
  function change(e) {
    Promise.all([...e.target.files].map(JSZip.loadAsync))
      .then((x) =>
        x.reduce(
          (acc, cur) => acc.then((x) => mergeFiles(x, cur)),
          Promise.resolve()
        )
      )
      .then((x) => x.generateAsync({ type: "blob" }))
      .then((x) => saveAs(x, "hello.zip"));
  }

  return React.createElement("input", {
    onChange: change,
    type: "file",
    multiple: true,
  });
};

ReactDOM.render(
  React.createElement(MultipleZipMerger, null),
  document.getElementById("root")
);

  
</script>
</body>
</html>

Instead of downloading a file in pieces, I suggest you to do it in one piece through single endpoint.

Anyways, you can try this solution, I am not completely sure if it works as i do not have the access to real API of yours, and what kind of data it is sending.

You just need to correct the response_type of your axios call.

Promise.all([
  axios.get(
    `https://testapi.com/folder/file.zip`,
    { responseType: 'stream', headers: { Accept: "application/json",} }
  ), 
  axios.get(
    `https://testapi.com/folder/file.z01`,
    { responseType: 'stream', headers: { Accept: "application/json",} }
  )
]).then((response) => {
  let blob:any = []
    response.forEach((e:any) => {
      blob.push(e.data)
    })
    const newBlob = new Blob(blob, {type: "octet/stream"});
    saveAs(newBlob, "zip.zip")
  });

I hope this would work with the correct response type.

Related