'Zip File Download via Axios from Express Server not working in React App

I have implemented a backend where administrators can create backups. These backups are collected in zip files and are stored in a non-public folder on the server.

The administrators should be able to download the zip files afterwards. But this does not work properly. The file is downloaded, but when you try to unzip it, you get the message "An attempt was made to move the file pointer before the beginning of the file". The zip file does not seem to be the problem. If you unpack it on the server, the unpacking works without errors. There must be something wrong with the download process.

  const downloadHandler = (params) => {
    axios.get(`/backup/download?name=${params.filename}`).then((res) => {
      const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
      const link = document.createElement('a');
      link.href = downloadUrl;
      link.setAttribute('download', 'file.zip');
      document.body.appendChild(link);
      link.click();
      link.remove();
    });
  };

The downloadHandler shown above is called in the React app as soon as a user clicks on the download button of a backup. After that the download menu of the browser opens for direct download.

Server implementation:

router.route('/backup/download').get((req, res, next) => {
  try {
    res.download(path.join(backupPath, req.query.name));
  } catch (err) {
    next(err);
  }
});

I have already spent hours looking for a solution, but no matter what I try, it doesn't work. The problem is, as I said, that the file cannot be unzipped after downloading.



Solution 1:[1]

I've been having some trouble to make a similar use case work, here is what I came up with after mixing up several tutorials.

On the server side I use Express with Archiver to generate a zip file :

router.post("/generate", onlyAdmin, async(req, res) => {
    try {
        const archive = archiver("zip", {
            zlib: { level: 9 }, // Sets the compression level.
        });

        archive.on("error", function(error) {
            console.log(error);
            res.status(500).send(error);
            return;
        });
        archive.on("end", function() {
            console.log("Archive wrote %d bytes", archive.pointer());
        });

        const files = [{
                content: "aaaa",
                name: "Custom/DB/ChroniclesDB.xml",
            },
            {
                content: "bbbc",
                name: "Custom/Locales/Locales.xml",
            },
        ];

        res.attachment("Data.zip");
        archive.pipe(res);

        files.forEach((file) => {
            archive.append(file.content, { name: file.name });
        });

        archive.finalize();
        return;
    } catch (error) {
        console.log(error);
        res.status(500).send(error);
        return;
    }
});

On the client side I have a React/Redux app with the following code :

const generate_selected = (ids) => (dispatch) => {
    let url = ApiPaths.Generate;

    var data = {
        ids: ids,
    };
    axios
        .post(url, data, {
            responseType: "arraybuffer",
        })
        .then((response) => {
            if (response.data) {
                dispatch(data_loaded(response));
            }
        })
        .catch((error) => {
            if (error.response) {
                dispatch(errorWhileGenerating(error.response.data));
            } else if (error.request) {
                console.log(error.request);
            } else {
                console.log("Error", error.message);
            }
        });
};

data_loaded: (state, action) => {
    let res = action.payload;
    const url = window.URL.createObjectURL(
        new Blob([res.data], {
            type: res.headers["content-type"],
        })
    );

    const actualFileName = getFileNameFromContentDisposition(
        res.headers["content-disposition"]
    );

    // uses the download attribute on a temporary anchor to trigger the browser
    // download behavior. if you need wider compatibility, you can replace this
    // part with a library such as filesaver.js
    const link = document.createElement("a");
    link.href = url;
    link.setAttribute("download", actualFileName);
    document.body.appendChild(link);
    link.click();
    link.parentNode.removeChild(link)
},

function getFileName(contentDisposition) {
    if (!contentDisposition) return null;

    const match = contentDisposition.match(/filename="?([^"]+)"?/);
    return match ? match[1] : null;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ciaanh