Скачивание файлов в 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.jpg6. Использование сторонних библиотек
Готовые библиотеки, такие как download, axios, got, node-fetch, часто упрощают работу:
- download — тонкая оболочка для загрузки в папку и сохранения с именем файла;
- got — мощная и гибкая, поддерживает редиректы, таймауты, retry и стримы;
- axios / node-fetch — универсальные HTTP-клиенты, но требуют дополнительной обработки потоков.
Пример с пакетом download:
npm install downloadconst 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 и инфраструктуру.
Похожие материалы
Microsoft Teams не синхронизируется с Outlook — решение
Как найти ссылки на другие книги в Excel
Создать ленту времени проекта в Excel
Xbox Captures: как записывать и управлять кадрами
Проверка: включён ли JavaScript в браузере