How to download a file with Node.js (without using third-party libraries)?

后端 未结 28 1278
逝去的感伤
逝去的感伤 2020-11-22 03:37

How do I download a file with Node.js without using third-party libraries?

I don\'t need anything special. I only want to download a file from a giv

相关标签:
28条回答
  • 2020-11-22 04:16

    Vince Yuan's code is great but it seems to be something wrong.

    function download(url, dest, callback) {
        var file = fs.createWriteStream(dest);
        var request = http.get(url, function (response) {
            response.pipe(file);
            file.on('finish', function () {
                file.close(callback); // close() is async, call callback after close completes.
            });
            file.on('error', function (err) {
                fs.unlink(dest); // Delete the file async. (But we don't check the result)
                if (callback)
                    callback(err.message);
            });
        });
    }
    
    0 讨论(0)
  • 2020-11-22 04:16

    ✅So if you use pipeline, it would close all other streams and make sure that there are no memory leaks.

    Working example:

    const http = require('http');
    const { pipeline } = require('stream');
    const fs = require('fs');
    
    const file = fs.createWriteStream('./file.jpg');
    
    http.get('http://via.placeholder.com/150/92c952', response => {
      pipeline(
        response,
        file,
        err => {
          if (err)
            console.error('Pipeline failed.', err);
          else
            console.log('Pipeline succeeded.');
        }
      );
    });
    

    From my answer to "What's the difference between .pipe and .pipeline on streams".

    0 讨论(0)
  • 2020-11-22 04:16

    Writing my own solution since the existing didn't fit my requirements.

    What this covers:

    • HTTPS download (switch package to http for HTTP downloads)
    • Promise based function
    • Handle forwarded path (status 302)
    • Browser header - required on a few CDNs
    • Filename from URL (as well as hardcoded)
    • Error handling

    It's typed, it's safer. Feel free to drop the types if you're working with plain JS (no Flow, no TS) or convert to a .d.ts file

    index.js

    import httpsDownload from httpsDownload;
    httpsDownload('https://example.com/file.zip', './');
    

    httpsDownload.[js|ts]

    import https from "https";
    import fs from "fs";
    import path from "path";
    
    function download(
      url: string,
      folder?: string,
      filename?: string
    ): Promise<void> {
      return new Promise((resolve, reject) => {
        const req = https
          .request(url, { headers: { "User-Agent": "javascript" } }, (response) => {
            if (response.statusCode === 302 && response.headers.location != null) {
              download(
                buildNextUrl(url, response.headers.location),
                folder,
                filename
              )
                .then(resolve)
                .catch(reject);
              return;
            }
    
            const file = fs.createWriteStream(
              buildDestinationPath(url, folder, filename)
            );
            response.pipe(file);
            file.on("finish", () => {
              file.close();
              resolve();
            });
          })
          .on("error", reject);
        req.end();
      });
    }
    
    function buildNextUrl(current: string, next: string) {
      const isNextUrlAbsolute = RegExp("^(?:[a-z]+:)?//").test(next);
      if (isNextUrlAbsolute) {
        return next;
      } else {
        const currentURL = new URL(current);
        const fullHost = `${currentURL.protocol}//${currentURL.hostname}${
          currentURL.port ? ":" + currentURL.port : ""
        }`;
        return `${fullHost}${next}`;
      }
    }
    
    function buildDestinationPath(url: string, folder?: string, filename?: string) {
      return path.join(folder ?? "./", filename ?? generateFilenameFromPath(url));
    }
    
    function generateFilenameFromPath(url: string): string {
      const urlParts = url.split("/");
      return urlParts[urlParts.length - 1] ?? "";
    }
    
    export default download;
    
    0 讨论(0)
  • 2020-11-22 04:17

    Based on the other answers above and some subtle issues, here is my attempt.

    1. Only create the fs.createWriteStream if you get a 200 OK status code. This reduces the amount of fs.unlink commands required to tidy up temporary file handles.
    2. Even on a 200 OK we can still possibly reject due to an EEXIST file already exists.
    3. Recursively call download if you get a 301 Moved Permanently or 302 Found (Moved Temporarily) redirect following the link location provided in the header.
    4. The issue with some of the other answers recursively calling download was that they called resolve(download) instead of download(...).then(() => resolve()) so the Promise would return before the download actually finished. This way the nested chain of promises resolve in the correct order.
    5. It might seem cool to clean up the temp file asynchronously, but I chose to reject only after that completed too so I know that everything start to finish is done when this promise resolves or rejects.
    const https = require('https');
    const fs = require('fs');
    
    /**
     * Download a resource from `url` to `dest`.
     * @param {string} url - Valid URL to attempt download of resource
     * @param {string} dest - Valid path to save the file.
     * @returns {Promise<void>} - Returns asynchronously when successfully completed download
     */
    function download(url, dest) {
      return new Promise((resolve, reject) => {
        const request = https.get(url, response => {
          if (response.statusCode === 200) {
     
            const file = fs.createWriteStream(dest, { flags: 'wx' });
            file.on('finish', () => resolve());
            file.on('error', err => {
              file.close();
              if (err.code === 'EEXIST') reject('File already exists');
              else fs.unlink(dest, () => reject(err.message)); // Delete temp file
            });
            response.pipe(file);
          } else if (response.statusCode === 302 || response.statusCode === 301) {
            //Recursively follow redirects, only a 200 will resolve.
            download(response.headers.location, dest).then(() => resolve());
          } else {
            reject(`Server responded with ${response.statusCode}: ${response.statusMessage}`);
          }
        });
    
        request.on('error', err => {
          reject(err.message);
        });
      });
    }
    
    0 讨论(0)
  • 2020-11-22 04:18

    You can create an HTTP GET request and pipe its response into a writable file stream:

    const http = require('http');
    const fs = require('fs');
    
    const file = fs.createWriteStream("file.jpg");
    const request = http.get("http://i3.ytimg.com/vi/J---aiyznGQ/mqdefault.jpg", function(response) {
      response.pipe(file);
    });
    

    If you want to support gathering information on the command line--like specifying a target file or directory, or URL--check out something like Commander.

    0 讨论(0)
  • 2020-11-22 04:18

    Here's yet another way to handle it without 3rd party dependency and also searching for redirects:

            var download = function(url, dest, cb) {
                var file = fs.createWriteStream(dest);
                https.get(url, function(response) {
                    if ([301,302].indexOf(response.statusCode) !== -1) {
                        body = [];
                        download(response.headers.location, dest, cb);
                      }
                  response.pipe(file);
                  file.on('finish', function() {
                    file.close(cb);  // close() is async, call cb after close completes.
                  });
                });
              }
    
    
    0 讨论(0)
提交回复
热议问题