7

I want to implement a big file downloading (approx. 10-1024 Mb) from the same server (without external cloud file storage, aka on-premises) where my app runs using Node.js and Express.js.

I figured out how to do that by converting the entire file into Blob, transferring it over the network, and then generating a download link with window.URL.createObjectURL(…) for the Blob. Such approach perfectly works as long as the files are small, otherwise it is impossible to keep the entire Blob in the RAM of neither server, nor client.

I've tried to implement several other approaches with File API and AJAX, but it looks like Chrome loads the entire file into RAM and only then dumps it to the disk. Again, it might be OK for small files, but for big ones it's not an option.

My last attempt was to send a basic Get-request:

const aTag = document.createElement("a");
aTag.href = `/downloadDocument?fileUUID=${fileName}`;
aTag.download = fileName;
aTag.click();

On the server-side:

app.mjs

app.get("/downloadDocument", async (req, res) => {

    req.headers.range = "bytes=0";

    const [urlPrefix, fileUUID] = req.url.split("/downloadDocument?fileUUID=");

    const downloadResult = await StorageDriver.fileDownload(fileUUID, req, res);

});

StorageDriver.mjs

export const fileDownload = async function fileDownload(fileUUID, req, res) {

    //e.g. C:\Users\User\Projects\POC\assets\wanted_file.pdf
    const assetsPath = _resolveAbsoluteAssetsPath(fileUUID);

    const options = {
        dotfiles: "deny",
        headers: {
            "Content-Disposition": "form-data; name=\"files\"",
            "Content-Type": "application/pdf",
            "x-sent": true,
            "x-timestamp": Date.now()
        }
    };

    res.sendFile(assetsPath, options, (err) => {

        if (err) {
            console.log(err);
        } else {
            console.log("Sent");
        }

    });

};

When I click on the link, Chrome shows the file in Downloads but with a status Failed - No file. No file appears in the download destination.

My questions:

  1. Why in case of sending a Get-request I get Failed - No file?
  2. As far as I understand, res.sendFile can be a right choice for small files, but for big-ones it's better to use res.write, which can be split into chunks. Is it possible to use res.write with Get-request?

P.S. I've elaborated this question to make it more narrow and clear. Previously this question was focused on downloading a big file from Dropbox without storing it in the RAM, the answer can be found: How to download a big file from Dropbox with Node.js?

Mike B.
  • 10,955
  • 19
  • 76
  • 118

1 Answers1

1

Chrome can't show nice progress of downloading because the file is downloading on the background. And after downloading, a link to the file is created and "clicked" to force Chrome to show the dialog for the already downloaded file.

It can be done more easily. You need to create a GET request and let the browser download the file, without ajax.

app.get("/download", async (req, res, next) => {
  const { fileName } = req.query;
  const downloadResult = await StorageDriver.fileDownload(fileName);
  res.set('Content-Type', 'application/pdf');
  res.send(downloadResult.fileBinary);
});
function fileDownload(fileName) {
  const a = document.createElement("a");
  a.href = `/download?fileName=${fileName}`;
  a.download = fileName;
  a.click();
}
artanik
  • 2,279
  • 14
  • 19
  • I've tried your proposal, it downloads a file but the behaviour remains the same, firstly Chrome downloads the entire file into RAM and only then dumps it to the disk. Perhaps there is some header, which should ask browser to download a file directly to the disk? – Mike B. Jun 28 '20 at 13:54
  • I'm not sure, why Chrome downloads the whole file to RAM in this case. Try to open this URL in a new tab and check whether or not it download the file into RAM. When I've tried this code in my little express app, the file was downloaded after the dialog, when I clicked "OK" I saw the progress. If you still using the `$.ajax` to download the file, then I bet you have the same behaviuor, otherwise, I have no idea why it's still the same :( – artanik Jun 28 '20 at 17:19
  • I tried to re-implement your solution, I send a `GET`-request, on the server I catch it and build an absolute path to the file. Then I try to send the file to a client via `res.download(fileAbsPath)` but Chrome returns "Failed - No file". When I try to open a file with an absolute path `fileAbsPath` (locally on Windows), then everything is working. Do you have, perhaps, any idea why Chromes return No file? – Mike B. Aug 11 '20 at 14:56
  • Maybe, this file at `fileAbsPath` does not exist on the server, try to read a file to debug this issue using [`readFile`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback) or pass an additional callback to [`res.download`](https://expressjs.com/en/api.html#res.download) to get more info about this error. – artanik Aug 11 '20 at 15:20
  • In `res.download` callback I get `RangeNotSatisfiableError: Range Not Satisfiable`. If I type `fileAbsPath` in browser's address bar, I get the file. – Mike B. Aug 11 '20 at 15:28
  • 1
    [Here is an example](https://codesandbox.io/s/shy-leftpad-ydubq?file=/src/index.js:0-1100) with `sendFile` which is under `res.download`. The first link isn't working, because of the abs path, but the next link is fine. I've changed the`root` in `options`. – artanik Aug 11 '20 at 15:49
  • I've implemented your code snippet and now I get `Failed - No file`, so strange. I've never assumed that downloading a file from a local host could be such a complicated task. – Mike B. Aug 11 '20 at 16:45
  • 1
    I ran out of ideas. Can you isolate your issue on `codesandbox`? Just a downloading part with the same logic. Maybe I can help you to debug this part. – artanik Aug 11 '20 at 17:10
  • I think I found the reason for `Failed - No file`: `req.headers.range = "bytes=0";`, after removing it everything is working! BTW, do I understand it correctly that for the big files `res.write` is a better option over `res.sendFile`? – Mike B. Aug 12 '20 at 07:07
  • 1
    It seems equal to me. They're both based on `stream` under the hood. [`res.write`](https://nodejs.org/api/http.html#http_response_write_chunk_encoding_callback), according to docs, can "stream" a file, as well as `res.sendFile` use this package called [`send`](https://www.npmjs.com/package/send) which is based on [`fs.createReadStream`](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options) ([source code](https://github.com/pillarjs/send/blob/master/index.js)). But I'm not 100% sure, maybe those streams are different in some ways. – artanik Aug 12 '20 at 08:03
  • I've implemented your code, it works but at the end of a file transferring I get `Error: Request aborted. Code: 'ECONNABORTED'` on a server side. I've checked already, there is no `res.end()` call at all, which might lead to such issue. Why Node.js closes the connection? This happens only to the big files (50 MB+) – Mike B. Aug 12 '20 at 21:30
  • 1
    I've found some issues on GitHub, but these issues related to body size _(in other words uploading)_, https://github.com/expressjs/express/issues/4237 https://stackoverflow.com/questions/13374238/how-to-limit-upload-file-size-in-express-js – artanik Aug 13 '20 at 05:44
  • Thanks god, for uploading I'm using `multer`, who is working perfectly smooth! – Mike B. Aug 13 '20 at 07:40