Home » Как работать с ZIP-файлами в Node.js
Как работать с ZIP-файлами в Node.js

Как работать с 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.

Основные рекомендации:

  • Всегда используйте потоки для больших файлов
  • Не забывайте про валидацию и ограничения
  • Обрабатывайте ошибки корректно
  • Тестируйте производительность на реальных данных
  • Мониторьте использование памяти

Эти инструменты помогут создать надёжную систему работы с архивами, которая будет эффективно работать в продакшене. Особенно полезно для систем бэкапов, деплоя и обработки пользовательского контента.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked