6

Как скачать файл с помощью Node.js (без использования сторонних библиотек)?

3

Как скачать файл с помощью Node.js, не используя сторонние библиотеки?

Мне не нужно ничего специфичного. Я просто хочу скачать файл по указанному URL и сохранить его в заданной директории.

5 ответ(ов)

5

Не забудьте обрабатывать ошибки! Следующий код основан на ответе Агусто Романа.

var http = require('http');
var fs = require('fs');

var download = function(url, dest, cb) {
  var file = fs.createWriteStream(dest);
  var request = http.get(url, function(response) {
    response.pipe(file);
    file.on('finish', function() {
      file.close(cb);  // close() - асинхронная функция, вызываем cb после завершения close.
    });
  }).on('error', function(err) { // Обработка ошибок
    fs.unlink(dest); // Удаляем файл асинхронно. (Но не проверяем результат)
    if (cb) cb(err.message);
  });
};

Этот код обеспечивает скачивание файла по указанному URL и сохранение его в указанное местоположение. Важно тщательно обрабатывать ошибки: если возникает ошибка при выполнении HTTP-запроса, файл, который мог быть частично загружен, будет удалён, и функция обратного вызова (cb) будет вызвана с сообщением об ошибке.

1

Конечно! Вот перевод ответа в стиле StackOverflow:


Ваш код для загрузки файла с использованием http и fs выглядит неплохо, но имейте в виду, что если не дождаться события finish, вы можете получить неполный файл. Ниже приведён улучшенный вариант с учётом правильного контроля потока и использования callback.

var http = require('http');
var fs = require('fs');

var download = function(url, dest, cb) {
  var file = fs.createWriteStream(dest);
  
  http.get(url, function(response) {
    response.pipe(file);
    
    // Дожидаемся завершения записи, чтобы вызвать callback
    file.on('finish', function() {
      file.close(cb); // Передаём cb в close
    });
    
  }).on('error', function(err) { // Обработка ошибок
    fs.unlink(dest); // Удаляем файл, если возникла ошибка
    if (cb) cb(err.message); // Вызываем callback с ошибкой
  });
}

Обратите внимание, что я добавил обработку ошибок, чтобы при сбое загрузки файл удалялся, а колбек вызывался с сообщением об ошибке. Также благодаря подсказке @Augusto Roman важно передать cb в метод file.close, а не вызывать его напрямую. Это гарантирует, что колбек будет вызван только после успешного завершения записи файла.


Надеюсь, это поможет! Если у вас возникнут дополнительные вопросы, не стесняйтесь спрашивать.

0

В ответе gfxmonk действительно наблюдается "гонка данных" между вызовом колбэка и завершением file.close(). Фактически, file.close() принимает колбэк, который вызывается после завершения операции закрытия. В противном случае, немедленное использование файла может привести к сбоям (хотя это происходит довольно редко!).

Полное решение будет выглядеть следующим образом:

var http = require('http');
var fs = require('fs');

var download = function(url, dest, cb) {
  var file = fs.createWriteStream(dest);
  var request = http.get(url, function(response) {
    response.pipe(file);
    file.on('finish', function() {
      file.close(cb);  // close() является асинхронным, вызываем cb после завершения close.
    });
  });
}

Если не дожидаться события finish, простые скрипты могут оказаться с неполным файлом. Не расписав вызов колбэка cb через close, вы можете столкнуться с гонкой между доступом к файлу и тем, что файл на самом деле не готов к использованию.

0

Похоже, что node.js изменился, и действительно есть некоторые проблемы с другими решениями (используя node v8.1.2):

  1. Не нужно вызывать file.close() в событии finish. По умолчанию fs.createWriteStream настроен на автоматическое закрытие: https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options
  2. file.close() следует вызывать при возникновении ошибки. Возможно, это не нужно, если файл удаляется (unlink()), но как правило это необходимо: https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
  3. Временный файл не удаляется, если statusCode !== 200.
  4. fs.unlink() без колбэка считается устаревшим (выводит предупреждение).
  5. Если файл назначения (dest) существует, он будет перезаписан.

Ниже приведено модифицированное решение (используя ES6 и промисы), которое учитывает указанные проблемы.

const http = require("http");
const fs = require("fs");

function download(url, dest) {
    return new Promise((resolve, reject) => {
        const file = fs.createWriteStream(dest, { flags: "wx" });

        const request = http.get(url, response => {
            if (response.statusCode === 200) {
                response.pipe(file);
            } else {
                file.close();  // Закрываем файл при ошибочном ответе
                fs.unlink(dest, () => {}); // Удаляем временный файл
                reject(`Сервер ответил кодом ${response.statusCode}: ${response.statusMessage}`);
            }
        });

        request.on("error", err => {
            file.close();  // Закрываем файл при ошибке запроса
            fs.unlink(dest, () => {}); // Удаляем временный файл
            reject(err.message);
        });

        file.on("finish", () => {
            resolve();  // Загруженный файл успешно завершен
        });

        file.on("error", err => {
            file.close();  // Закрываем файл при ошибке записи

            if (err.code === "EEXIST") {
                reject("Файл уже существует");
            } else {
                fs.unlink(dest, () => {}); // Удаляем временный файл
                reject(err.message);
            }
        });
    });
}

Это решение исправляет перечисленные проблемы и предоставляет более надежный способ для скачивания файлов с использованием Node.js.

0

На основе других ответов и некоторых тонких моментов, вот мой подход.

  1. Проверьте, что файл не существует перед обращением к сети, используя fs.access.
  2. Создавайте fs.createWriteStream только если получен статус-код 200 OK. Это уменьшает количество команд fs.unlink, необходимых для очистки временных файлов.
  3. Даже при статусе 200 OK мы всё равно можем получить reject из-за ошибки EEXIST, если файл уже существует (представьте, что другой процесс создал файл, пока мы выполняли сетевые запросы).
  4. Рекурсивно вызывайте download, если получаете 301 Moved Permanently или 302 Found (Moved Temporarily), следуя за ссылкой, указанной в заголовке.
  5. Проблема с некоторыми другими ответами, которые рекурсивно вызывали download, заключалась в том, что они вызывали resolve(download) вместо download(...).then(() => resolve()), поэтому Promise возвращался до того, как загрузка завершилась. Таким образом, вложенная цепочка обещаний разрешалась в правильном порядке.
  6. Может показаться классным очищать временный файл асинхронно, но я выбрал отклонение только после завершения этой операции, чтобы быть уверенным, что всё завершено от начала до конца, когда это обещание разрешается или отклоняется.
const https = require('https');
const fs = require('fs');

/**
 * Загружает ресурс по `url` в `dest`.
 * @param {string} url - Действительный URL для загрузки ресурса
 * @param {string} dest - Действительный путь для сохранения файла.
 * @returns {Promise<void>} - Возвращает асинхронно при успешном завершении загрузки
 */
function download(url, dest) {
  return new Promise((resolve, reject) => {
    // Проверка, что файл еще не существует перед обращением к сети
    fs.access(dest, fs.constants.F_OK, (err) => {

        if (err === null) 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('Файл уже существует');
                else fs.unlink(dest, () => reject(err.message)); // Удаляем временный файл
              });
              response.pipe(file);
            } else if (response.statusCode === 302 || response.statusCode === 301) {
              // Рекурсивно следуем за редиректами, только 200 разрешит промис.
              download(response.headers.location, dest).then(() => resolve());
            } else {
              reject(`Сервер ответил с кодом ${response.statusCode}: ${response.statusMessage}`);
            }
          });
      
          request.on('error', err => {
            reject(err.message);
          });
    });
  });
}
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь