Перейти к содержимому
Поиск
Разработка

Archiver: ZIP-архивы на Node.js — мастерская и 8 практических сценариев

ZIP для отчётов, кабинетов и скачивания: восемь сценариев Archiver, реальные архивы, проверка SHA-256, конструктор кода и полный Node.js-проект.

Александр Некрасов
Archiver — ZIP-архивы, файлы и потоки в Node.js

Archiver 8 на практике: ZIP из строк, файлов и потоков, HTTP-скачивание, сжатие и отмена. Восемь сценариев, настоящие архивы, мини-конструктор кода и исходники для Windows, Linux и VPS.

Что такое Archiver и зачем собирать ZIP в Node.js

Пользователь выбрал несколько документов и хочет скачать их одной кнопкой. Менеджеру нужен пакет отчётов за месяц, клиенту — фотографии вместе с описанием, разработчику — набор сгенерированных JSON и CSV. Отдельная ссылка на каждый файл быстро становится неудобной. ZIP позволяет передать связанный набор с понятными именами и структурой каталогов.

Archiver — библиотека Node.js для создания архивов. Она принимает строки, Buffer, потоки и файлы, добавляет записи и выдаёт результат как поток байтов. Этот поток можно сохранить на диск или направить в HTTP-ответ. Приложение определяет, какие данные разрешено включить, а библиотека занимается форматом архива и сжатием.

В статье используем Archiver 8.0.0 и актуальный ESM-импорт. Подготовлен самостоятельный проект с CLI, ограниченным локальным HTTP endpoint и восемью сценариями мастерской. На странице доступны настоящие ZIP, состав файлов, контрольные суммы и конструктор кода. Исходники позволяют повторить генерацию у себя на Windows или Linux.

Где применять: кабинеты, отчёты, медиа и интеграции

Самый понятный сценарий — кнопка «Скачать всё» в личном кабинете. Человек получает документы одной операции: счёт, акт, пояснение и машинный JSON. В системе отчётности архив объединяет выгрузки подразделений, а в медиасервисе — обработанные изображения и файл с параметрами. Названия внутри пакета помогают понять содержание без открытия каждого документа.

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

Для больших выгрузок полезно разделить подготовку и скачивание. Фоновая задача создаёт архив, проверяет его и сохраняет в хранилище. Интерфейс показывает состояние и выдаёт ссылку, когда результат готов. Это уменьшает зависимость от длительного открытого HTTP-соединения и позволяет повторить обработку после временной ошибки.

Что библиотека делает, а что остаётся задачей приложения

Archiver создаёт архивный поток, но не определяет права доступа к документам. Если сервер получает список идентификаторов, он должен проверить каждый объект и владельца. Нельзя превращать параметр запроса в произвольный путь к файлу сервера. Библиотека не знает, является выбранный файл публичным отчётом или приватной конфигурацией.

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

В проекте демонстрируется создание ZIP, а не распаковка произвольных пользовательских архивов. Инспектор читает небольшие собственные результаты без извлечения на диск. Сервис загрузки недоверенных ZIP требует других ограничений: контроля распакованного размера, количества записей, путей и ресурсов. Эти границы стоит определить до добавления кнопки загрузки в интерфейс.

Мини-мастерская: восемь сценариев и настоящие архивы

Переключайте сценарии и варианты: текстовые данные, папка клиента, выборка CSV, уровни сжатия, пакет с манифестом, имена записей, HTTP-выдача и отмена. Для завершённых операций доступен настоящий ZIP. Список записей показывает имена, исходный и сжатый размер, CRC32 и текстовый предпросмотр. В сравнении сжатия используются реальные размеры одного набора данных.

Архивы заранее созданы настоящим Archiver в Node.js, затем прочитаны независимым ZIP-инспектором. Страница не запускает Node.js на сервере сайта по нажатию кнопки. Она показывает записанные результаты, позволяет скачать файлы и действительно вычисляет SHA-256 загруженного ZIP в вашем браузере. Для собственного серверного запуска есть полный проект.

Мини-конструктор ниже примеров создаёт самостоятельный JavaScript-файл. Измените имя записи, текст и уровень DEFLATE, затем скачайте код для запуска у себя. Эти поля не меняют заранее подготовленные ZIP — интерфейс явно разделяет результаты примера и создаваемый вами скрипт. Отмена и ошибка имени не выдают частичный файл за готовый архив.

Установка Archiver 8 и отличие от старого API

Восьмая версия использует ESM и именованные классы. Для ZIP нужен ZipArchive. Старый пример с require('archiver') и вызовом archiver('zip') нельзя механически перенести в этот проект. При чтении руководств сверяйте версию: часть примеров в интернете относится к прежнему интерфейсу библиотеки.

Проект рассчитан на Node.js 24.15+. Версия самой библиотеки закреплена точно, а package-lock.json сохраняет полный набор зависимостей. Для готового архива используйте npm ci, чтобы восстановить этот набор. Команда build заново создаёт все учебные ZIP и записи результатов, включая запросы к временному локальному HTTP-серверу.

В зависимостях зафиксирован override minimatch 11.0.1 из-за опубликованного предупреждения для более ранней версии, которую подтягивает readdir-glob. Совместимость directory и glob проверена тестами этого проекта; пользовательские шаблоны endpoint не принимает. Причина и ссылка на advisory приведены в README. После обновления зависимостей override нужно пересмотреть и повторить проверки.

npm install --save-exact archiver@8.0.0

// JavaScript, файл .mjs:
import { ZipArchive } from 'archiver';
const archive = new ZipArchive({ zlib: { level: 6 } });

Жизненный цикл: источник, архив и выходной поток

Операция состоит из нескольких этапов. Сначала создаётся выходной поток, затем экземпляр Archiver, обработчики ошибок и ожидание завершения. После подключения потоков добавляются записи. Вызов finalize сообщает, что новых записей больше не будет, однако очередь и выходной поток ещё должны завершить работу.

Нельзя считать архив готовым только потому, что вызван finalize или обработана последняя входная запись. Для файла важно дождаться завершения выходного потока; для HTTP — учитывать передачу и возможный разрыв соединения. В проекте используется finished из стандартного Node.js API и совместное ожидание результата финализации.

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

import { ZipArchive } from 'archiver';
import { createWriteStream } from 'node:fs';
import { finished } from 'node:stream/promises';

const output = createWriteStream('report.zip', { flags: 'wx' });
const archive = new ZipArchive({ zlib: { level: 6 } });
// Обработчики error/warning и отмены устанавливаются здесь.
const completed = finished(output, { cleanup: true });
completed.catch(() => {});
archive.pipe(output);
archive.append('Учебный отчёт', { name: 'README.txt' });
await Promise.all([archive.finalize(), completed]);

Пример 1. Строки и Buffer без временных входных файлов

Для небольшого отчёта не обязательно сначала создавать отдельные файлы на диске. Строку можно передать прямо в append, а JSON преобразовать в Buffer. Это удобно, когда данные уже получены из базы или вычислены приложением. В мастерской базовый вариант содержит README и JSON, дополнительный — ещё и CSV.

Имя записи внутри ZIP задаётся отдельно от содержимого. Пользователь увидит именно его при открытии архива. Выбирайте понятные расширения и стабильную структуру: report.json для машинного чтения, README.txt для пояснения и таблицу для ручного просмотра. В JSON полезно сохранять версию схемы, если файл будет обрабатываться другими программами.

Передача строки или Buffer всё равно занимает память процесса. Такой подход удобен для небольших данных, но многогигабайтный экспорт не следует целиком собирать в одном Buffer. Для больших наборов используйте потоковую генерацию и проверяйте поведение при ошибке посреди выдачи. Наличие архивного потока само по себе не делает все источники потоковыми.

archive.append('Все данные учебные.\n', {
  name: 'README.txt', date: fixedDate
});
archive.append(Buffer.from(JSON.stringify(rows, null, 2)), {
  name: 'report.json', date: fixedDate
});

Существующий файл и собственный Readable

Для уже подготовленного документа есть метод file. Он принимает путь на сервере и имя записи в архиве. Archiver использует ленивое открытие файла, что помогает не держать сразу множество файловых дескрипторов. При этом путь должен поступать из разрешённой серверной модели, а не напрямую из пользовательского запроса.

Другой вариант — передать собственный Readable в append. Источником может стать генератор строк, результат преобразования или поток чтения. У каждого такого источника должен быть владелец, который понимает, как остановить его при отмене. Если источник продолжает читать сеть или базу после закрытия скачивания, процесс расходует ресурсы впустую.

В учебном опыте отмены Readable выдаёт небольшие блоки и проверяет сигнал остановки. При ошибке модуль уничтожает созданный им поток. Для внешних источников уточните их API завершения: уничтожение Node-потока не всегда автоматически отменяет отдельный запрос к базе, внешнему сервису или запущенный процесс.

archive.file(allowedPdfPath, {
  name: 'documents/invoice.pdf'
});
archive.append(ownedReadable, {
  name: 'export/data.csv'
});

Пример 2. Каталог клиента и вложенные папки

Метод directory позволяет включить разрешённый каталог рекурсивно. В мастерской это небольшой набор с README, техническим описанием и SVG. Один вариант помещает его в папку client-pack, другой раскладывает содержимое в корень архива. Откройте список записей и сравните пути: префикс меняет организацию пакета, не содержание файлов.

Перед упаковкой проект проверяет собственный каталог fixtures: допускаются обычные файлы и папки, символические ссылки отклоняются, количество и суммарный размер ограничены. Это защита конкретного учебного набора. Для изменяемого production-каталога важны права записи и возможность замены файлов между проверкой и чтением; не считайте предварительную проверку универсальным решением любых гонок.

Даты в примерах зафиксированы, а права задаются явно: каталогам нужен проход, файлам — чтение. Порядок directory зависит от файловой системы. Поэтому полный проект для одинакового результата Windows/Linux использует отсортированный обход и file с готовыми метаданными; фрагмент ниже показывает более короткий вариант. В реальном экспорте отдельно решите, нужно ли сохранять исходные даты и права.

archive.directory(allowedDirectory, 'client-pack', entry => ({
  ...entry,
  date: fixedDate,
  mode: entry.stats.isDirectory() ? 0o755 : 0o644
}));
// Содержимое непосредственно в корне:
// archive.directory(allowedDirectory, false, ...);

Пример 3. Glob: включить нужные CSV и исключить лишнее

Иногда нужен не весь каталог, а определённый набор файлов. В третьем сценарии используются заранее заданные шаблоны: все CSV или только CSV из monthly. Отдельная папка private исключена. Учебный файл внутри неё не содержит секретов; он нужен, чтобы результат исключения можно было проверить по составу ZIP.

Параметр cwd задаёт исходный каталог, а prefix — путь внутри архива. Это разные пространства имён. Разрешённый серверный каталог не должен превращаться в часть публичного пути документа, а имя внутри ZIP не должно позволять выход за корень при последующей распаковке.

Не принимайте произвольный glob от посетителя без отдельной модели ограничений. Даже если запрещён выход за каталог, сложные шаблоны способны расходовать значительные ресурсы. Наш endpoint принимает идентификатор готового сценария, а шаблон выбирается кодом. После изменения зависимости minimatch этот путь дополнительно проверяется реальными архивами и их содержимым.

archive.glob('**/*.csv', {
  cwd: allowedDirectory,
  dot: false,
  ignore: ['private/**']
}, {
  prefix: 'reports',
  date: fixedDate,
  mode: 0o644
});

Пример 4. STORE и DEFLATE: сравниваем реальный результат

Сжатие особенно эффективно для повторяющегося текста. Поэтому мастерская использует один учебный текстовый набор и создаёт четыре ZIP: STORE без сжатия и DEFLATE с уровнями один, шесть и девять. Размеры измерены после завершения архива. Можно скачать варианты и убедиться, что распакованное содержимое одинаковое.

Уровень девять не означает универсально лучший выбор. Он может потребовать больше процессорного времени при небольшой дополнительной экономии. Уже сжатые JPEG, PNG, видео и некоторые PDF часто уменьшаются мало. Для таких данных STORE иногда полезнее повторной компрессии, особенно если важна скорость выдачи.

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

const compressed = new ZipArchive({ zlib: { level: 6 } });
const stored = new ZipArchive({ store: true });
// Оба получают один и тот же исходный набор.
// Сравнивать размер следует после завершения выходных потоков.

Пример 5. Манифест: что вошло в пакет и как это проверить

Если архив будет передаваться между сервисами, полезно добавить manifest.json. В нём перечисляются имена документов, размеры и SHA-256 исходного содержимого. Получатель может проверить, что нужные файлы присутствуют и соответствуют ожидаемому набору. В мастерской есть вариант с папками и вариант с плоскими именами.

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

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

const manifest = {
  schema: 1,
  files: files.map(file => ({
    name: file.name,
    bytes: Buffer.byteLength(file.text),
    sha256: createHash('sha256').update(file.text).digest('hex')
  }))
};
archive.append(JSON.stringify(manifest, null, 2), {
  name: 'manifest.json', date: fixedDate
});

Пример 6. Имена записей и защита от неоднозначных путей

Имя внутри ZIP должно быть относительным и понятным. Записи с ../, абсолютным путём или буквой диска могут стать опасными при неосторожной распаковке. Даже если ваш инструмент только создаёт архив, разумно выдавать набор, который не требует от получателя угадывать намерения автора.

Проверка мастерской запрещает выход через родительский каталог, обратную косую черту, пустые сегменты, управляющие символы и зарезервированные Windows-имена. Кириллица при этом разрешена. Попробуйте reports/отчёт.txt, затем ../secret.txt: конструктор покажет ошибку и отключит скачивание кода до исправления имени.

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

const segments = name.split('/');
if (name.startsWith('/') || name.includes('\\') ||
    segments.some(part => !part || part === '.' || part === '..')) {
  throw new Error('Нужен безопасный относительный путь');
}
// Полная политика, включая Windows-имена, — src/validation.mjs.

Пример 7. ZIP в HTTP-ответе и кнопка скачивания

Локальный сервер проекта действительно формирует ZIP для POST-запроса. Вход содержит только scene и variant из разрешённого списка. До начала ответа проверяются параметры, затем устанавливаются application/zip и Content-Disposition с именем файла. Результат направляется в response как поток, поэтому заранее вычислять Content-Length не требуется.

В мастерской HTTP-сценарий содержит архив и заголовки, полученные настоящим запросом во время сборки. Это проверяемая запись результата, а не выполнение endpoint на публичном сайте. Для повторения запустите Node-сервер у себя и выполните запрос из README. Публичная страница остаётся обычной статикой.

Интерфейс приложения должен отличать успешное скачивание от прерванной передачи. Если ошибка произошла после отправки заголовков, сервер уже не может заменить начавшийся ZIP обычным JSON-ответом с другим статусом. Соединение завершается, а клиент должен считать файл неполным. Для ответственных выгрузок часто удобнее сначала подготовить готовый файл.

response.setHeader('Content-Type', 'application/zip');
response.setHeader('Content-Disposition',
  'attachment; filename="documents.zip"');
response.setHeader('Cache-Control', 'no-store');
await writeArchive(response, {
  scene: 'http', variant: 'csv'
}, { signal: controller.signal });

Пример 8. Отмена и освобождение входных потоков

Человек может закрыть вкладку, отменить загрузку или потерять сеть. Продолжать сжимать большой набор после этого часто бессмысленно. Сервер отслеживает закрытие ответа и передаёт AbortSignal операции. Модуль останавливает Archiver, уничтожает выходной поток и закрывает созданные им входные Readable.

Метод abort у Archiver выполняет остановку по принципу best effort и не обязан дочитывать оставшиеся источники. Поэтому одного вызова недостаточно, если приложение создало собственные внешние потоки. Их жизненный цикл должен быть явно связан с операцией. В мастерской отмена после первого блока записана как cancelled, без ссылки на незавершённый ZIP.

Тесты проверяют и прямой AbortController, и настоящий разрыв HTTP-клиента. Это важно: хороший результат на обычном успешном скачивании не доказывает корректную очистку при обрыве. Для файлового результата CLI пишет во временное имя с расширением partial и публикует окончательное имя только после успешного завершения.

response.on('close', () => {
  if (!response.writableFinished) {
    controller.abort(new Error('Клиент закрыл соединение'));
  }
});
// В обработчике отмены операции:
archive.abort();
for (const source of ownedSources) source.destroy();
output.destroy();

Прогресс и размер: какие числа действительно известны

Archiver сообщает о обработанных записях и количестве выданных байтов. Эти показатели полезны для журнала и интерфейса, но не всегда превращаются в точный процент готовности. Полный размер сжатого результата заранее неизвестен, а число входных файлов при обходе каталога может уточняться постепенно.

Разделяйте этапы: выбор документов, чтение, сжатие, завершение архива и передача пользователю. Счётчик обработанных записей относится к одному этапу, а загрузка браузера — к другому. Последняя обработанная запись ещё не гарантирует, что весь ZIP доставлен на устройство. В интерфейсе лучше использовать конкретные подписи вместо одного неопределённого процента.

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

archive.on('progress', progress => {
  console.log({
    processedEntries: progress.entries.processed,
    knownEntries: progress.entries.total,
    emittedBytes: archive.pointer()
  });
});

Ошибки и warning: когда неполный набор недопустим

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

Событие warning тоже требует решения приложения. Для необязательного вспомогательного файла пропуск иногда допустим, но пакет документов договора не должен тихо потерять один акт. Поэтому проект считает warning неуспехом. В другой системе можно собирать предупреждения в итоговый отчёт, однако это должно быть частью явного контракта.

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

archive.on('warning', error => fail(error));
archive.on('error', error => fail(error));
output.on('error', error => fail(error));
// fail должен быть идемпотентным: повторное событие
// не запускает рекурсивную очистку заново.

Лимиты, параллельность и входные параметры endpoint

Потоковая выдача уменьшает необходимость держать весь ZIP в памяти, но процесс всё равно использует CPU, файловые дескрипторы и буферы. Неограниченное количество одновременных выгрузок способно перегрузить сервер. Ограничивайте число задач, объём входа, количество документов и максимальное время обработки.

Учебный endpoint принимает JSON не больше 1024 байт, допускает две операции одновременно и имеет таймаут десять секунд. Он слушает только loopback, проверяет локальный Host и не разрешает запросы другого происхождения. Прикладные данные выбираются по фиксированным идентификаторам, произвольные пути и glob-шаблоны отсутствуют.

Эти ограничения делают локальный опыт предсказуемым, но не заменяют production-авторизацию. Для публичного кабинета нужны аккаунт, права на каждый документ, журнал операций и согласованные доверенные адреса reverse proxy. Настройки нагрузки выбираются по измерениям реального набора, а не копированием двух учебных чисел в любой проект.

Запуск в Windows и создание первого архива

Распакуйте проект, откройте PowerShell в его каталоге и проверьте Node.js 24.15+. Установка из lockfile восстанавливает точные зависимости, тесты проверяют формат и ошибки, а build создаёт учебные результаты. Команда archive запускает настоящий CLI и сохраняет ZIP в собственный runtime проекта.

Локальный сервер открывает мастерскую на 127.0.0.1:8173. Если порт занят, задайте другой через PORT. Сам интерфейс и endpoint находятся в одном процессе, но публичная версия статьи использует только статическую часть. Открытие локальной страницы не запускает генерацию автоматически; для endpoint выполните отдельный запрос.

Чтобы попробовать конструктор, скачайте create-archive.mjs в каталог проекта и запустите Node. Скрипт не перезаписывает существующий my-report.zip благодаря режиму wx. После успешного запуска откройте архив обычным архиватором и сравните имя записи и текст. Для повторного опыта выберите новое выходное имя либо осознанно удалите собственный предыдущий результат.

node --version
npm.cmd ci --ignore-scripts
npm.cmd test
npm.cmd run build
npm.cmd run archive -- memory csv
npm.cmd start
# Другой порт:
# $env:PORT = '8176'
# npm.cmd start

Linux: CLI, файлы и повторяемая сборка

На Linux используются те же исходники и lockfile. Node.js нужен для генерации и локального HTTP endpoint; дополнительных системных архиваторов проект не вызывает. Результаты можно проверить любым независимым ZIP-инструментом, а автоматические тесты уже распаковывают записи в память и сравнивают контрольные суммы.

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

В примерах фиксированы даты и режимы записей, чтобы уменьшить случайные различия сборки. Однако смена Node.js, zlib или зависимостей способна изменить двоичное представление ZIP при одинаковом содержимом. Для проверки переноса между платформами полезно сравнивать и целый архив, и распакованные данные, имена, размеры и CRC.

node --version
npm ci --ignore-scripts
npm test
npm run build
npm run archive -- manifest tree
node src/cli.mjs --help
# Готовый файл находится в runtime/.
npm start

VPS: статика, локальный сервис и безопасное подключение

Для опубликованной мастерской достаточно скопировать public в каталог сайта. Node.js на сервере не требуется: браузер получает готовые ZIP, JSON и конструктор кода. В существующем Caddy можно настроить обычную раздачу файлов по HTTPS. Подробности домена и проксирования разобраны в практической статье о Caddy.

Если нужен самостоятельный Node endpoint, разместите исходники вне публичного document root и запустите отдельным служебным пользователем. Готовому серверу не требуются dev-зависимости инспектора. Для повторной сборки они понадобятся. Пример unit systemd и команды диагностики находятся в README.

Учебный endpoint остаётся на loopback. Для удалённой проверки удобно использовать SSH-туннель и обращаться к 127.0.0.1 на своём компьютере. Настоящая публичная интеграция требует отдельной настройки авторизации, Host/Origin и reverse proxy. Простое открытие порта не превращает локальный пример в готовый сервис для любых посетителей.

archiver.example.com {
    root * /var/www/archiver/public
    encode zstd gzip
    file_server
}

# SSH-туннель к запущенному локальному endpoint на VPS:
# ssh -L 8173:127.0.0.1:8173 user@server

Когда вынести подготовку архива в очередь задач

Потоковый ответ удобен для небольшого набора, который готовится быстро. Если выгрузка занимает минуты, зависит от внешних API или объединяет множество документов, лучше создать фоновую задачу. Интерфейс получает идентификатор, показывает состояние и выдаёт ссылку после проверки результата.

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

Заранее определите срок жизни архива и механизм очистки. Готовая ссылка должна проверять права пользователя и не жить бесконечно без необходимости. Для регулярных отчётов возможен запуск по расписанию, например через node-cron, но расписание само по себе не решает блокировки, повтор после сбоя и контроль успешного результата.

CRC32, SHA-256 и проверка готового ZIP

Факт существования файла не доказывает, что он является завершённым архивом. ZIP содержит служебную структуру с перечнем записей; прерванный поток может не иметь корректного окончания. Поэтому сборщик мастерской открывает каждый готовый результат отдельной библиотекой yauzl и читает содержимое записей.

CRC32 сравнивается с фактически распакованными байтами. В тесте один байт намеренно повреждается, и инспекция должна обнаружить расхождение. SHA-256 всего файла помогает проверить, что скачан именно опубликованный архив. В браузере кнопка выполняет реальную загрузку и вычисляет эту сумму через Web Crypto.

Контрольная сумма не заменяет проверку состава: корректный ZIP может содержать не те документы. Для рабочего экспорта сравнивайте ожидаемые имена и количество записей, а при необходимости — манифест и прикладные поля. Инспектор проекта ограничен собственными небольшими архивами и не извлекает их на файловую систему.

const digest = await crypto.subtle.digest('SHA-256', zipArrayBuffer);
const hex = [...new Uint8Array(digest)]
  .map(byte => byte.toString(16).padStart(2, '0'))
  .join('');
// Сравнить hex с ожидаемой суммой опубликованного ZIP.

Тесты и проверка поведения при ошибках

В проект включены тринадцать проверок, которые используют настоящую библиотеку и настоящие ZIP. Проверяется содержимое строк и Buffer, структура каталогов, glob-исключения, методы STORE и DEFLATE, манифест, имена записей и одинаковый результат для фиксированного простого набора. Отдельно проверяется намеренно повреждённый архив.

Важная часть набора посвящена неуспеху: ошибка выходного потока, AbortController, ограничения HTTP и настоящий разрыв клиентского соединения. Эти сценарии показывают, что операция не сообщает ложный успех и прекращает работу. Успешная обычная загрузка не покрывает такие случаи автоматически.

После тестов проверьте сам интерфейс: выбор всех сцен, скачивание ZIP и JSON, проверку SHA-256, ошибочное имя конструктора и запуск скачанного кода. На узком экране контролы должны оставаться читаемыми, а длинный исходник прокручиваться внутри блока. Проверка ширины окна не заменяет отдельную проверку физического мобильного браузера и его загрузок.

npm test
npm run build
npm audit
# Windows: те же команды через npm.cmd.
# Затем открыть мастерскую и проверить реальные скачивания.

Как адаптировать проект и где читать дальше

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

В архиве есть HTML5-мастерская, локальный Bootstrap, исходные модули Node.js, CLI, endpoint, тесты, fixtures и README для Windows/Linux/VPS. Собственный код распространяется по MIT, уведомления зависимостей включены в поставку. Демонстрационные данные вымышленные и пригодны для безопасного повторения опытов.

Для API восьмой версии сверяйтесь с исходниками соответствующего тега, а общие операции и события изучайте по документации Archiver. Жизненный цикл потоков и HTTP лучше проверять по документации используемой версии Node.js. После обновления зависимостей повторите создание, инспекцию и скачивание архивов, чтобы изменения не нарушили формат, на который рассчитывают пользователи.