Гид по технологиям

Скачивание файлов в Node.js на локальный диск

5 min read Node.js Обновлено 05 Dec 2025
Скачивание файлов в Node.js на локальный диск
Скачивание файлов в Node.js на локальный диск

Две стопки файлов и документов, расположенные рядом

Зачем скачивать файлы локально

Скачивание файлов на локальный диск даёт простоту доступа, работу без интернета и полный контроль над данными. Это удобно для бэкапов, предобработки больших бинарных файлов, записи в систему CI/CD и для случаев, когда внешнее облачное хранилище мешает рабочему процессу.

Короткая дефиниция: Node.js — серверная платформа на базе V8, позволяющая работать с сетью и файловой системой через модули вроде fs, https и path.

Важно: если вы работаете с приватными облачными объектами (S3, Azure Blob и др.), предпочтительнее использовать официальные SDK с поддержкой подписи и политики безопасности.

1. Загрузка файла без сторонних библиотек

Для базовой загрузки нужны три модуля: fs, https и path. fs управляет файлами, https делает GET-запросы, path помогает с именами и путями.

Простой пример функции, которая скачивает файл и сохраняет его в текущую папку:

const https = require('https');
const fs = require('fs');
const path = require('path');

function downloadFile(url) {
  const filename = path.basename(new URL(url).pathname);
  const fileStream = fs.createWriteStream(filename);

  https.get(url, (res) => {
    if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
      console.error(`HTTP error: ${res.statusCode} for ${url}`);
      res.resume(); // сброс потока
      fileStream.close();
      fs.unlink(filename, () => {}); // удалить частичный файл
      return;
    }

    res.pipe(fileStream);

    fileStream.on('finish', () => {
      fileStream.close();
      console.log(`Загрузка завершена: ${filename}`);
    });

    fileStream.on('error', (err) => {
      console.error(`Ошибка записи файла ${filename}:`, err.message);
      fileStream.close();
      fs.unlink(filename, () => {});
    });
  }).on('error', (err) => {
    console.error(`Ошибка запроса ${url}:`, err.message);
  });
}

// Пример вызова
// downloadFile('https://example.com/file.zip');

Пояснения и рекомендации:

  • Используйте new URL(url).pathname и path.basename для корректного получения имени файла из URL.
  • Всегда проверяйте res.statusCode и обрабатывайте коды за пределами 200–299.
  • Подписывайтесь на событие error у потоков и у запроса, чтобы не допустить висящих дескрипторов.
  • При ошибке удаления частичного файла безопасно игнорируйте ошибку fs.unlink в callback.

2. Обработка редиректов и HTTP (не только HTTPS)

Node.js модуль https не следует за 3xx редиректами автоматически. Для поддержки редиректов без сторонних библиотек можно добавить простую логику:

const http = require('http');
const https = require('https');

function getLib(url) {
  return url.startsWith('https') ? https : http;
}

function downloadWithRedirects(url, maxRedirects = 5) {
  const lib = getLib(url);
  lib.get(url, (res) => {
    if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && maxRedirects > 0) {
      const nextUrl = new URL(res.headers.location, url).toString();
      res.destroy();
      return downloadWithRedirects(nextUrl, maxRedirects - 1);
    }
    // дальнейшая обработка как выше
  }).on('error', (err) => console.error(err));
}

Небольшая эвристика: лимит редиректов 5–10 — безопасный баланс между корректностью и зацикливанием.

3. Контроль прогресса и content-length

Если сервер отдаёт заголовок Content-Length, вы можете показать прогресс загрузки:

https.get(url, (res) => {
  const total = parseInt(res.headers['content-length'] || '0', 10);
  let downloaded = 0;
  res.on('data', (chunk) => {
    downloaded += chunk.length;
    if (total) {
      const pct = ((downloaded / total) * 100).toFixed(1);
      process.stdout.write(`\r${pct}% (${downloaded}/${total} байт)`);
    }
  });
  // pipe в файл и завершение аналогично
});

Если Content-Length отсутствует, можно показывать скорость по накопленным байтам и времени.

4. Многопоточность и параллелизм при загрузке нескольких файлов

Для массовых загрузок важно ограничить число параллельных соединений. Простой семафор:

async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean).catch(clean);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

// Использование:
// const tasks = urls.map(url => () => downloadFilePromise(url));
// await limitConcurrency(tasks, 5);

Альтернатива: использовать готовые библиотеки p-limit или async.

5. Полный пример скрипта: cli, папка вывода, параллелизм, повторы

Ниже собранный пример, который обрабатывает аргументы командной строки, поддерживает выходную папку, ограничивает параллелизм и делает несколько попыток при неудаче.

// file: downloader.js
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');

const args = process.argv.slice(2);
if (!args.length) {
  console.error('Использование: node downloader.js output_dir url1 url2 ...');
  process.exit(1);
}

const outDir = args[0];
const urls = args.slice(1);
fs.mkdirSync(outDir, { recursive: true });

function getLib(url) {
  return url.startsWith('https') ? https : http;
}

function downloadFilePromise(url, retries = 2) {
  return new Promise((resolve, reject) => {
    const lib = getLib(url);
    const filename = path.basename(new URL(url).pathname) || 'file';
    const outPath = path.join(outDir, filename);
    const req = lib.get(url, (res) => {
      if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
        res.resume();
        if (retries > 0 && String(res.statusCode).startsWith('5')) {
          return resolve(downloadFilePromise(url, retries - 1));
        }
        return reject(new Error(`HTTP ${res.statusCode}`));
      }

      const fileStream = fs.createWriteStream(outPath);
      res.pipe(fileStream);

      fileStream.on('finish', () => {
        fileStream.close(() => resolve(outPath));
      });

      fileStream.on('error', (err) => {
        fileStream.close();
        fs.unlink(outPath, () => reject(err));
      });
    });

    req.on('error', (err) => reject(err));
    req.setTimeout(30000, () => {
      req.destroy(new Error('Timeout'));
    });
  });
}

async function main() {
  const tasks = urls.map(url => () => downloadFilePromise(url, 2));
  const limit = 4;
  await limitConcurrency(tasks, limit);
  console.log('Все задачи завершены');
}

// Реализована ранее функция limitConcurrency
async function limitConcurrency(tasks, limit) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean).catch(clean);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

main().catch(err => {
  console.error('Ошибка выполнения:', err.message);
  process.exit(1);
});

Запуск:

node downloader.js downloads https://example.com/a.zip https://example.com/b.jpg

6. Использование сторонних библиотек

Готовые библиотеки, такие как download, axios, got, node-fetch, часто упрощают работу:

  • download — тонкая оболочка для загрузки в папку и сохранения с именем файла;
  • got — мощная и гибкая, поддерживает редиректы, таймауты, retry и стримы;
  • axios / node-fetch — универсальные HTTP-клиенты, но требуют дополнительной обработки потоков.

Пример с пакетом download:

npm install download
const download = require('download');
const urls = process.argv.slice(2);
(async () => {
  await Promise.all(urls.map(url => download(url, 'files')));
})();

Преимущество: быстрее в разработке. Недостаток: ещё одна зависимость, которую нужно поддерживать и обновлять.

7. Частые ошибки и как их избегать

  • Необработанные события ‘error’ у потоков и запросов приводят к утечкам дескрипторов. Всегда подписывайтесь на error.
  • Игнорирование HTTP-кодов — сервер мог вернуть 403 или 404.
  • Отсутствие очистки частичных файлов при ошибке — это накопит мусор.
  • Попытки скачать очень большие файлы в память — используйте стримы и запись в файл по частям.
  • Неправильные имена файлов из URL, содержащие query-параметры. Применяйте new URL(url).pathname и декодируйте имя.

8. Когда локальная загрузка не подходит

  • Много терабайтных данных: лучше использовать инструменты синхронизации (rclone, s3 cp).
  • Требуются транзакционные гарантии и контроль версий — используйте хранилище с версионностью.
  • Нужна высокая параллельность и масштаб — лучше строить очередь задач и распределять через worker-пулы.

9. Тесты и критерии приёмки

Критерии приёмки:

  • Скрипт корректно скачивает 3 разных файла (малый текст, средний бинарный, большой потоковый).
  • При сетевой ошибке происходит не более N повторов и в конце корректный код ошибки.
  • Частичные файлы удаляются при неудаче.
  • Параллелизм ограничен и не превышает заданное значение.

Минимальные тестовые сценарии:

  • Успешная загрузка одного файла.
  • Попытка загрузить несуществующий URL (404) — скрипт не падает необработанной ошибкой.
  • Симуляция таймаута — повторная попытка и корректный выход.
  • Проверка создания целевой папки и права записи.

10. Чек-листы для ролей

Для разработчика:

  • Код обрабатывает ошибки у потоков и HTTP-запросов.
  • Имя файла безопасно и не содержит query-параметров.
  • Есть ограничение параллелизма и таймауты.

Для оператора:

  • Логи содержат URL, имя файла и итог (OK/ERROR).
  • Настроен мониторинг на количество ошибок загрузки.
  • Есть процесс сборки мусора для частичных файлов.

11. Альтернативные подходы

  • Использовать официальный SDK облака при загрузке из приватных хранилищ (S3, GCS, Azure).
  • Для потоковой обработки без сохранения на диск используйте стрим-пайплайны и обработку в памяти (внимание на RAM).
  • Для массовой миграции данных применяйте специализированные утилиты (rclone, aws s3 sync).

12. Примеры отказов и отладка

Если загрузка зависает:

  • Проверьте, открыты ли исходящие порты и не блокирует ли межсетевой экран.
  • Убедитесь, что сервер поддерживает нужный протокол (http/https).
  • Проверьте заголовки ответа и наличие Content-Length.

Если получаете неверный файл:

  • Проверьте редиректы и заголовок Content-Type.
  • Сравните хеши локального и удалённого файлов.

13. Краткое резюме

  • Для простых задач хватит fs + http/https + path.
  • Для удобства и надежности используйте библиотеки типа got или download.
  • Всегда обрабатывайте ошибки, очищайте частичные файлы и ограничивайте параллелизм.
  • Тестируйте сценарии с ошибками и таймаутами.

Важно: перед автоматизированной массовой загрузкой проверьте правила использования удалённого сервера — частые параллельные запросы могут выглядеть как DDoS.

Дополнительные ресурсы и шаблоны можно адаптировать под ваш CI/CD и инфраструктуру.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Microsoft Teams не синхронизируется с Outlook — решение
Поддержка

Microsoft Teams не синхронизируется с Outlook — решение

Как найти ссылки на другие книги в Excel
Excel

Как найти ссылки на другие книги в Excel

Создать ленту времени проекта в Excel
Excel

Создать ленту времени проекта в Excel

Xbox Captures: как записывать и управлять кадрами
Игры

Xbox Captures: как записывать и управлять кадрами

Проверка: включён ли JavaScript в браузере
браузер

Проверка: включён ли JavaScript в браузере

Миграция Skype for Business на Microsoft Teams
IT Migration

Миграция Skype for Business на Microsoft Teams