- Home »

Как работать с ZIP-файлами в Node.js
Архивы везде. Лог-файлы, бэкапы, скрипты деплоя — всё это рано или поздно попадёт в ZIP. И когда нужно автоматизировать обработку архивов на сервере, Node.js становится отличным выбором. Сегодня разберём как работать с ZIP-файлами в Node.js: от простого извлечения до сложных операций с потоками данных. Это пригодится для создания систем бэкапов, автоматизации деплоя, обработки пользовательских файлов и многого другого.
Как это работает под капотом
ZIP-архивы в Node.js обрабатываются через несколько популярных библиотек. Основные игроки:
- yauzl — для чтения ZIP-архивов (легковесная и быстрая)
- yazl — для создания архивов (от того же автора)
- adm-zip — универсальная библиотека (проще в использовании)
- archiver — мощная библиотека для создания архивов
- node-stream-zip — работает с потоками, отлично для больших файлов
Большинство этих библиотек работают на основе потоков (streams), что позволяет обрабатывать большие архивы без загрузки всего содержимого в память. Это критично для серверных приложений, где RAM — ценный ресурс.
Быстрый старт: устанавливаем и настраиваем
Начнём с самого простого — установим основные пакеты:
npm init -y
npm install adm-zip archiver node-stream-zip
npm install --save-dev @types/node
Создаём базовую структуру проекта:
mkdir zip-toolkit
cd zip-toolkit
npm init -y
touch index.js
mkdir test-files
echo "Hello World" > test-files/hello.txt
echo "console.log('test')" > test-files/script.js
Чтение ZIP-архивов: практические примеры
Начнём с самого частого кейса — извлечения файлов из архива. Вот пример с adm-zip
:
const AdmZip = require('adm-zip');
const fs = require('fs');
const path = require('path');
// Простое извлечение всех файлов
function extractAllFiles(zipPath, outputDir) {
try {
const zip = new AdmZip(zipPath);
zip.extractAllTo(outputDir, true);
console.log(`Извлечено в ${outputDir}`);
} catch (error) {
console.error('Ошибка при извлечении:', error.message);
}
}
// Извлечение конкретного файла
function extractSpecificFile(zipPath, fileName, outputPath) {
const zip = new AdmZip(zipPath);
const entry = zip.getEntry(fileName);
if (entry) {
zip.extractEntryTo(entry, outputPath, false, true);
console.log(`Файл ${fileName} извлечён`);
} else {
console.log(`Файл ${fileName} не найден в архиве`);
}
}
// Получение информации о содержимом архива
function getArchiveInfo(zipPath) {
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
console.log('Содержимое архива:');
entries.forEach(entry => {
console.log(`- ${entry.entryName} (${entry.header.size} bytes)`);
});
}
// Использование
extractAllFiles('./archive.zip', './extracted');
extractSpecificFile('./archive.zip', 'config.json', './');
getArchiveInfo('./archive.zip');
Для больших архивов лучше использовать node-stream-zip
:
const StreamZip = require('node-stream-zip');
async function extractLargeArchive(zipPath, outputDir) {
const zip = new StreamZip.async({ file: zipPath });
try {
// Получаем информацию о файлах
const entries = await zip.entries();
console.log(`Найдено ${Object.keys(entries).length} файлов`);
// Извлекаем всё
await zip.extract(null, outputDir);
console.log('Извлечение завершено');
// Закрываем архив
await zip.close();
} catch (error) {
console.error('Ошибка:', error);
}
}
// Извлечение с фильтрацией
async function extractWithFilter(zipPath, outputDir, filterFn) {
const zip = new StreamZip.async({ file: zipPath });
try {
const entries = await zip.entries();
for (const entry of Object.values(entries)) {
if (filterFn(entry)) {
await zip.extract(entry, path.join(outputDir, entry.name));
console.log(`Извлечён: ${entry.name}`);
}
}
await zip.close();
} catch (error) {
console.error('Ошибка:', error);
}
}
// Использование с фильтром (только .js файлы)
extractWithFilter('./archive.zip', './js-files',
entry => entry.name.endsWith('.js'));
Создание ZIP-архивов
Теперь научимся создавать архивы. Для простых случаев подойдёт adm-zip
:
const AdmZip = require('adm-zip');
const fs = require('fs');
const path = require('path');
// Создание архива из папки
function createZipFromDirectory(sourceDir, outputZip) {
const zip = new AdmZip();
function addDirectoryToZip(dirPath, zipPath = '') {
const items = fs.readdirSync(dirPath);
items.forEach(item => {
const itemPath = path.join(dirPath, item);
const zipItemPath = path.join(zipPath, item);
if (fs.statSync(itemPath).isDirectory()) {
addDirectoryToZip(itemPath, zipItemPath);
} else {
zip.addLocalFile(itemPath, zipPath);
}
});
}
addDirectoryToZip(sourceDir);
zip.writeZip(outputZip);
console.log(`Архив создан: ${outputZip}`);
}
// Создание архива из отдельных файлов
function createZipFromFiles(files, outputZip) {
const zip = new AdmZip();
files.forEach(file => {
if (fs.existsSync(file)) {
zip.addLocalFile(file);
}
});
zip.writeZip(outputZip);
console.log(`Архив с файлами создан: ${outputZip}`);
}
// Добавление содержимого из строки
function createZipWithContent(outputZip) {
const zip = new AdmZip();
zip.addFile('config.json', Buffer.from(JSON.stringify({
version: '1.0',
env: 'production'
}, null, 2)));
zip.addFile('readme.txt', Buffer.from('Это тестовый архив'));
zip.writeZip(outputZip);
console.log(`Архив с контентом создан: ${outputZip}`);
}
// Использование
createZipFromDirectory('./test-files', './directory-archive.zip');
createZipFromFiles(['./package.json', './index.js'], './files-archive.zip');
createZipWithContent('./content-archive.zip');
Для более сложных случаев используем archiver
:
const archiver = require('archiver');
const fs = require('fs');
// Создание архива с прогрессом
function createAdvancedZip(outputPath) {
const output = fs.createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 } // Максимальное сжатие
});
// Обработка событий
output.on('close', () => {
console.log(`Архив создан: ${archive.pointer()} байт`);
});
archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
console.warn('Предупреждение:', err);
} else {
throw err;
}
});
archive.on('error', (err) => {
throw err;
});
// Подключаем поток
archive.pipe(output);
// Добавляем файлы
archive.file('./package.json', { name: 'package.json' });
archive.directory('./test-files/', 'test-files');
// Добавляем содержимое из строки
archive.append('Лог-файл содержимое', { name: 'app.log' });
// Завершаем архив
archive.finalize();
}
createAdvancedZip('./advanced-archive.zip');
Сравнение библиотек
Библиотека | Размер | Скорость | Функциональность | Лучше для |
---|---|---|---|---|
adm-zip | Маленький | Средняя | Базовая | Простые операции |
node-stream-zip | Средний | Высокая | Чтение + потоки | Большие архивы |
archiver | Большой | Высокая | Расширенная | Создание архивов |
yauzl/yazl | Маленький | Высокая | Специализированная | Производительность |
Практические кейсы и автоматизация
Теперь рассмотрим реальные задачи, с которыми сталкиваются при работе с серверами:
Система бэкапов
const archiver = require('archiver');
const fs = require('fs');
const path = require('path');
class BackupManager {
constructor(backupDir = './backups') {
this.backupDir = backupDir;
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
}
async createBackup(sources, name) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = path.join(this.backupDir, `${name}-${timestamp}.zip`);
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(backupPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => {
console.log(`Бэкап создан: ${backupPath} (${archive.pointer()} байт)`);
resolve(backupPath);
});
archive.on('error', reject);
archive.pipe(output);
// Добавляем источники
sources.forEach(source => {
if (fs.statSync(source).isDirectory()) {
archive.directory(source, path.basename(source));
} else {
archive.file(source, { name: path.basename(source) });
}
});
archive.finalize();
});
}
async cleanOldBackups(maxAge = 7 * 24 * 60 * 60 * 1000) { // 7 дней
const files = fs.readdirSync(this.backupDir);
const now = Date.now();
files.forEach(file => {
const filePath = path.join(this.backupDir, file);
const stat = fs.statSync(filePath);
if (now - stat.mtime.getTime() > maxAge) {
fs.unlinkSync(filePath);
console.log(`Удалён старый бэкап: ${file}`);
}
});
}
}
// Использование
const backup = new BackupManager();
backup.createBackup(['./logs', './config'], 'daily-backup')
.then(() => backup.cleanOldBackups());
Автоматизация деплоя
const StreamZip = require('node-stream-zip');
const fs = require('fs');
const path = require('path');
class DeployManager {
constructor(deployDir = './deploy') {
this.deployDir = deployDir;
}
async deployFromZip(zipPath, options = {}) {
const { backup = true, validate = true } = options;
// Создаём бэкап текущей версии
if (backup && fs.existsSync(this.deployDir)) {
await this.createBackup();
}
// Валидация архива
if (validate && !await this.validateArchive(zipPath)) {
throw new Error('Архив не прошёл валидацию');
}
// Очищаем директорию деплоя
if (fs.existsSync(this.deployDir)) {
fs.rmSync(this.deployDir, { recursive: true, force: true });
}
fs.mkdirSync(this.deployDir, { recursive: true });
// Извлекаем архив
const zip = new StreamZip.async({ file: zipPath });
await zip.extract(null, this.deployDir);
await zip.close();
console.log('Деплой завершён успешно');
}
async validateArchive(zipPath) {
const zip = new StreamZip.async({ file: zipPath });
try {
const entries = await zip.entries();
// Проверяем наличие обязательных файлов
const requiredFiles = ['package.json', 'index.js'];
const hasRequired = requiredFiles.every(file =>
Object.keys(entries).some(entry => entry.includes(file))
);
await zip.close();
return hasRequired;
} catch (error) {
console.error('Ошибка валидации:', error);
return false;
}
}
async createBackup() {
const backup = new BackupManager();
await backup.createBackup([this.deployDir], 'pre-deploy');
}
}
// Использование
const deploy = new DeployManager('./production');
deploy.deployFromZip('./release.zip', { backup: true, validate: true });
Нестандартные способы использования
Вот несколько интересных трюков для продвинутых пользователей:
Стриминг обработка больших архивов
const StreamZip = require('node-stream-zip');
const crypto = require('crypto');
// Вычисление хешей файлов в архиве без извлечения
async function calculateArchiveHashes(zipPath) {
const zip = new StreamZip.async({ file: zipPath });
const hashes = {};
try {
const entries = await zip.entries();
for (const [name, entry] of Object.entries(entries)) {
if (!entry.isDirectory) {
const stream = await zip.stream(name);
const hash = crypto.createHash('sha256');
stream.on('data', chunk => hash.update(chunk));
await new Promise((resolve, reject) => {
stream.on('end', () => {
hashes[name] = hash.digest('hex');
resolve();
});
stream.on('error', reject);
});
}
}
await zip.close();
return hashes;
} catch (error) {
console.error('Ошибка вычисления хешей:', error);
throw error;
}
}
// Использование
calculateArchiveHashes('./archive.zip')
.then(hashes => {
console.log('Хеши файлов:');
Object.entries(hashes).forEach(([file, hash]) => {
console.log(`${file}: ${hash}`);
});
});
Интеграция с веб-сервером
const express = require('express');
const multer = require('multer');
const StreamZip = require('node-stream-zip');
const archiver = require('archiver');
const app = express();
const upload = multer({ dest: 'uploads/' });
// Эндпоинт для получения информации об архиве
app.post('/zip/info', upload.single('archive'), async (req, res) => {
try {
const zip = new StreamZip.async({ file: req.file.path });
const entries = await zip.entries();
const info = {
totalFiles: Object.keys(entries).length,
totalSize: Object.values(entries).reduce((sum, entry) => sum + entry.size, 0),
files: Object.values(entries).map(entry => ({
name: entry.name,
size: entry.size,
isDirectory: entry.isDirectory
}))
};
await zip.close();
res.json(info);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Эндпоинт для создания архива из загруженных файлов
app.post('/zip/create', upload.array('files'), (req, res) => {
res.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="archive.zip"'
});
const archive = archiver('zip', { zlib: { level: 9 } });
archive.on('error', err => {
res.status(500).send({ error: err.message });
});
archive.pipe(res);
req.files.forEach(file => {
archive.file(file.path, { name: file.originalname });
});
archive.finalize();
});
app.listen(3000, () => {
console.log('Сервер запущен на порту 3000');
});
Оптимизация и производительность
При работе с архивами на продакшн-серверах важно учитывать несколько моментов:
- Использование потоков — для больших файлов всегда используйте stream-based библиотеки
- Контроль памяти — не загружайте весь архив в память сразу
- Асинхронность — все операции должны быть неблокирующими
- Валидация — проверяйте архивы перед обработкой
- Ограничения — устанавливайте лимиты на размер архивов
Вот пример оптимизированного обработчика:
const StreamZip = require('node-stream-zip');
const fs = require('fs');
class OptimizedZipProcessor {
constructor(options = {}) {
this.maxSize = options.maxSize || 100 * 1024 * 1024; // 100MB
this.maxFiles = options.maxFiles || 1000;
this.allowedExtensions = options.allowedExtensions || null;
}
async processArchive(zipPath, processor) {
// Проверяем размер файла
const stat = fs.statSync(zipPath);
if (stat.size > this.maxSize) {
throw new Error('Архив слишком большой');
}
const zip = new StreamZip.async({ file: zipPath });
try {
const entries = await zip.entries();
const fileEntries = Object.values(entries).filter(e => !e.isDirectory);
// Проверяем количество файлов
if (fileEntries.length > this.maxFiles) {
throw new Error('Слишком много файлов в архиве');
}
// Обрабатываем файлы порционно
const batchSize = 10;
for (let i = 0; i < fileEntries.length; i += batchSize) {
const batch = fileEntries.slice(i, i + batchSize);
await Promise.all(batch.map(async (entry) => {
if (this.isAllowedFile(entry.name)) {
const stream = await zip.stream(entry.name);
await processor(entry.name, stream);
}
}));
}
await zip.close();
} catch (error) {
await zip.close();
throw error;
}
}
isAllowedFile(filename) {
if (!this.allowedExtensions) return true;
const ext = filename.split('.').pop().toLowerCase();
return this.allowedExtensions.includes(ext);
}
}
// Использование
const processor = new OptimizedZipProcessor({
maxSize: 50 * 1024 * 1024, // 50MB
maxFiles: 500,
allowedExtensions: ['txt', 'json', 'js', 'html']
});
processor.processArchive('./archive.zip', async (filename, stream) => {
console.log(`Обрабатываем: ${filename}`);
// Здесь ваша логика обработки файла
});
Интеграция с другими инструментами
ZIP-архивы часто используются совместно с другими инструментами:
- PM2 — для управления процессами обработки архивов
- Redis — для кеширования информации об архивах
- Docker — для создания образов с обработчиками
- Nginx — для проксирования загрузки больших архивов
Если вы планируете разворачивать подобные решения, рекомендую арендовать VPS с достаточным объёмом дисковой памяти или выделенный сервер для high-load проектов.
Альтернативные решения
Помимо ZIP, стоит рассмотреть и другие форматы:
- tar.gz — стандарт для Unix-систем, лучше сжатие
- 7z — отличное сжатие, но медленнее
- rar — проприетарный формат, только для чтения
Полезные ссылки:
Заключение и рекомендации
Работа с ZIP-архивами в Node.js открывает широкие возможности для автоматизации серверных задач. Для простых операций используйте adm-zip
, для больших архивов — node-stream-zip
, а для создания сложных архивов — archiver
.
Основные рекомендации:
- Всегда используйте потоки для больших файлов
- Не забывайте про валидацию и ограничения
- Обрабатывайте ошибки корректно
- Тестируйте производительность на реальных данных
- Мониторьте использование памяти
Эти инструменты помогут создать надёжную систему работы с архивами, которая будет эффективно работать в продакшене. Особенно полезно для систем бэкапов, деплоя и обработки пользовательского контента.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.