yazl vs adm-zip vs archiver vs node-zip vs zip-a-folder vs zip-dir
Node.js 压缩与打包方案深度对比:从流式处理到内存操作
yazladm-ziparchivernode-zipzip-a-folderzip-dir类似的npm包:

Node.js 压缩与打包方案深度对比:从流式处理到内存操作

adm-zip、archiver、node-zip、yazl、zip-a-folder 和 zip-dir 都是 Node.js 生态中用于处理 ZIP 压缩文件的库,但它们的设计理念和适用场景截然不同。archiver 和 yazl 采用流式(Streaming)架构,适合处理大文件或实时数据传输,能有效控制内存占用;adm-zip 则倾向于将整个文件加载到内存中进行操作,适合小文件的快速读写;而 zip-a-folder 和 zip-dir 是基于底层库封装的高层工具,旨在简化特定场景(如打包整个目录)的代码复杂度。node-zip 由于依赖原生模块且维护停滞,在现代开发中已不再推荐。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
yazl4,158,21338758.7 kB222 年前MIT
adm-zip02,183151 kB581 个月前MIT
archiver02,97939.6 kB1745 个月前MIT
node-zip0217-1911 年前-
zip-a-folder077147 kB02 个月前MIT
zip-dir047-206 年前MIT

Node.js 压缩库深度对比:流式架构 vs 内存操作

在 Node.js 后端开发中,处理 ZIP 压缩文件是常见需求,无论是构建产物打包、日志归档还是文件下载服务。然而,不同的库在底层实现上有着本质区别:有的将整个文件加载到内存,有的则使用流式处理。选错库可能导致服务器内存崩溃或安装失败。本文将深入对比 adm-zip、archiver、node-zip、yazl、zip-a-folder 和 zip-dir,帮助你做出正确的架构决策。

🏗️ 核心架构:内存加载 vs 流式处理

这是选择压缩库时最重要的考量因素。它决定了你的应用能处理多大的文件,以及是否会拖慢服务器。

adm-zip 采用全内存加载模式。

  • 它会一次性读取整个 ZIP 文件或将所有待压缩文件读入内存。
  • 优点是 API 简单,支持随机读取 ZIP 内的文件。
  • 缺点是处理大文件时极易导致内存溢出(OOM)。
// adm-zip: 加载整个文件到内存
const AdmZip = require('adm-zip');
const zip = new AdmZip();

// 将所有文件读入内存后再压缩
zip.addLocalFolder("./dist");
// 同步写入磁盘,阻塞事件循环
zip.writeZip("./archive.zip");

archiver 采用流式(Streaming)模式。

  • 文件数据像水流一样通过管道,读一点、压一点、写一点。
  • 内存占用极低,适合处理 GB 级文件或网络流。
  • 支持在压缩过程中动态添加文件。
// archiver: 流式处理
const archiver = require('archiver');
const fs = require('fs');
const output = fs.createWriteStream('./archive.zip');
const archive = archiver('zip', { zlib: { level: 9 } });

output.on('close', () => console.log('完成'));
archive.pipe(output);

// 流式添加文件,不占用大量内存
archive.directory('./dist', false);
archive.finalize();

yazl 同样是流式,但更底层。

  • 它是一个纯粹的 ZIP 生成器,只负责输出流,不负责读取文件系统的细节。
  • 你需要手动管理文件读取流并传递给 yazl。
  • 适合需要高度定制压缩流程的场景。
// yazl: 手动管理流
const yazl = require('yazl');
const fs = require('fs');
const zipFile = new yazl.ZipFile();

// 必须手动创建读取流并添加
zipFile.addReadStream(fs.createReadStream('file.txt'), 'file.txt');
zipFile.addReadStream(fs.createReadStream('img.png'), 'img.png');

zipFile.outputStream.pipe(fs.createWriteStream('output.zip'));
zipFile.end();

node-zip 基于原生 C++ 模块。

  • 它试图通过原生代码提升性能,但失去了 Node.js 的跨平台便利性。
  • 在现代 Node.js 版本中编译困难,且不支持流式 API 的灵活性。
// node-zip: 原生模块调用 (不推荐)
const zip = require('node-zip')();
zip.load(data); // 通常也需要加载数据到内存
// 缺乏现代流式支持

📂 目录打包:封装程度对比

实际开发中,我们经常需要“把一个文件夹打包”。不同库对此的抽象程度不同。

zip-a-folder 提供了最高层封装。

  • 专门为此场景设计,一行代码即可完成。
  • 底层通常基于 archiver 或 yazl 实现。
  • 适合脚本、CLI 工具或简单的后端接口。
// zip-a-folder: 极简 API
const zipAFolder = require('zip-a-folder');

// 直接打包整个文件夹,返回 Promise
await zipAFolder.zip('./src', './src-archive.zip');

zip-dir 提供回调风格封装。

  • 老牌的目录打包库,API 设计较旧(基于回调)。
  • 支持过滤文件,但灵活性不如直接使用 archiver。
// zip-dir: 回调风格
const zipdir = require('zip-dir');

zipdir('./src', { saveTo: './src-archive.zip' }, function (err, buffer) {
  if (err) throw err;
  console.log('打包完成');
});

archiver 需要手动配置,但最灵活。

  • 虽然没有“一键打包”函数,但通过 directory 方法可以精确控制哪些文件被包含、重命名或排除。
  • 适合复杂的生产环境需求。
// archiver: 灵活控制
const archive = archiver('zip');
// 第二个参数允许你重命名根目录或过滤文件
archive.directory('./src', 'my-project/src', (data) => {
  if (data.name.endsWith('.log')) return false; // 排除日志文件
  return data;
});

⚠️ 维护状态与风险评估

在引入依赖前,必须评估库的健康状况。

  • node-zip: 已废弃。该库最后更新时间久远,依赖的原生接口在现代 Node.js 版本中经常编译失败。严禁在新项目中使用。如果遇到旧项目依赖,应尽快迁移到 yazl 或 archiver。
  • adm-zip: 维护中,但有局限。它依然活跃,但由于其内存加载机制,不适合云原生环境或高并发大文件场景。仅建议在确定文件大小可控(如配置文件、小型构建产物)时使用。
  • archiver: 行业标准。拥有庞大的社区和插件生态,长期稳定更新,是处理流式压缩的首选。
  • yazl: 稳定底层库。由知名开发者维护,专注于“只做一件事并做好”,非常适合作为底层依赖。
  • zip-a-folder / zip-dir: 特定场景工具。它们依赖上述底层库。如果底层库更新,它们可能跟进较慢。对于简单任务没问题,但复杂需求建议直接调用底层库。

🚀 性能与内存表现实测逻辑

假设我们需要压缩一个 500MB 的日志文件夹。

  1. 使用 adm-zip:

    • 进程内存会瞬间飙升,可能接近 1GB(因为要同时持有未压缩和压缩数据)。
    • 如果服务器内存限制为 512MB,进程会直接崩溃(OOM Killed)。
    • 代码虽然短,但风险极高。
  2. 使用 archiver:

    • 内存占用保持在几十 MB 左右,因为它处理的是数据流(Chunk)。
    • 可以边读取磁盘边压缩边写入网络响应,用户无需等待全部压缩完成即可开始下载。
    • 代码稍多,但系统稳定性大幅提升。
  3. 使用 zip-a-folder:

    • 表现取决于其内部实现(通常调用 archiver),表现良好。
    • 但如果需要自定义压缩级别或过滤规则,其 API 可能不够用,此时需退回使用 archiver。

🛠️ 高级场景:动态内容与加密

某些场景需要压缩动态生成的内容(如数据库查询结果)或添加加密。

动态内容流式压缩: 只有流式库(archiver, yazl)能优雅处理。

// archiver: 压缩动态生成的字符串流
const { Readable } = require('stream');
const archive = archiver('zip');

// 创建一个虚拟流
const dataStream = new Readable({
  read() {
    this.push(JSON.stringify({ time: Date.now() }));
    this.push(null); // 结束流
  }
});

archive.append(dataStream, { name: 'live-data.json' });

加密支持:

  • 原生库大多不直接支持 AES 加密 ZIP。
  • archiver 可以通过插件或配合 crypto 模块在流层面处理,但标准的 ZIP 加密(ZipCrypto)支持有限。
  • 如果需要强加密,通常建议在压缩后对文件流进行加密,或使用专门的加密库配合 archiver 的输出流。
// 配合 crypto 进行流式加密 (概念示例)
const crypto = require('crypto');
const cipher = crypto.createCipher('aes-256-cbc', 'password');

archive.pipe(cipher).pipe(fs.createWriteStream('encrypted.zip'));

📊 选型决策矩阵

特性adm-ziparchiveryazlnode-zipzip-a-folderzip-dir
核心机制内存加载流式 (Stream)流式 (Stream)原生模块封装 (基于流)封装 (基于流)
大文件支持❌ 差 (易 OOM)✅ 优秀✅ 优秀⚠️ 一般✅ 优秀✅ 优秀
API 复杂度⭐ 简单⭐⭐ 中等⭐⭐⭐ 较繁琐⭐⭐ 中等⭐ 极简⭐ 简单
动态添加文件❌ 不支持✅ 支持✅ 支持❌ 不支持❌ 不支持❌ 不支持
解压功能✅ 支持❌ 不支持 (仅压缩)❌ 不支持 (仅压缩)✅ 支持❌ 不支持❌ 不支持
维护状态🟢 活跃🟢 活跃🟢 活跃🔴 废弃🟡 稳定🟡 稳定
推荐场景小文件、CLI 工具服务器、大文件、流底层库开发、定制勿用快速脚本旧项目维护

💡 最终建议

1. 默认首选 archiver 对于 90% 的后端开发场景(如 Express/Koa 中提供文件下载、日志归档、大数据处理),archiver 是最佳选择。它的流式架构保证了系统的稳定性,丰富的 API 能应对各种复杂需求,且社区活跃,遇到问题容易找到答案。

2. 仅在特定场景使用 adm-zip 如果你确定处理的文件非常小(例如配置文件、小型代码片段),且需要频繁读取 ZIP 包内的特定文件(解压功能),adm-zip 的同步 API 会让代码更简洁。但务必加上文件大小检查,防止内存爆炸。

3. 快速脚本选 zip-a-folder 如果是写一个一次性脚本,或者在 CI/CD 流程中简单打包产物,zip-a-folder 能减少样板代码,提高效率。

4. 坚决避开 node-zip 无论出于何种理由,不要在新项目中引入 node-zip。它的原生依赖是部署的噩梦,且没有提供任何现代流式处理的优势。

5. 底层定制选 yazl 如果你正在开发一个需要深度集成压缩功能的库,或者需要极致的轻量级依赖,yazl 提供了最纯净的流式生成接口。

总结:在现代 Node.js 架构中,流式处理(Stream)是王道。选择 archiver 或 yazl 不仅是为了性能,更是为了系统的可预测性和稳定性。让数据流动起来,而不是把它塞进内存里。

如何选择: yazl vs adm-zip vs archiver vs node-zip vs zip-a-folder vs zip-dir

  • yazl:

    当你需要一个轻量级、纯 JavaScript 实现的 ZIP 生成库,且希望完全控制流式输出时,选择 yazl。它比 archiver 更底层,没有多余的依赖,非常适合嵌入到其他库中或作为自定义流处理管道的一部分。如果你只需要生成 ZIP 而不需要解压功能,它是极佳的选择。

  • adm-zip:

    当你需要处理小型文件且追求代码简单直观时,选择 adm-zip。它允许你将整个 ZIP 文件加载到内存中进行同步或异步操作,API 非常易用,适合命令行工具或构建脚本中的临时压缩任务。但请避免在处理大文件(如超过几百兆)时使用它,因为这会导致内存溢出。

  • archiver:

    如果你需要处理大文件、实时流式传输数据,或者需要在压缩过程中动态添加文件(如日志轮转、服务器端动态生成报告),archiver 是首选。它基于流(Stream)构建,内存占用极低,且拥有最丰富的插件生态和支持多种压缩格式(不仅是 ZIP)。

  • node-zip:

    不要在新项目中使用 node-zip。该库依赖原生 C++ 模块,安装过程容易因环境差异失败,且已长期停止维护。其功能完全可以用纯 JavaScript 实现的 yazl 或 archiver 替代,后者更稳定且易于部署。

  • zip-a-folder:

    如果你的唯一需求是将某个本地文件夹快速压缩为一个 ZIP 文件,且不想编写繁琐的流式管道代码,zip-a-folder 是最快捷的解决方案。它底层通常依赖 archiver 或 yazl,将常用模式封装为简单的异步函数,适合脚本化任务。

  • zip-dir:

    类似于 zip-a-folder,当你需要快速打包目录且偏好基于回调(Callback)或简单 Promise 的 API 时,可以选择 zip-dir。它在某些旧项目中较为常见,但在新项目中,直接使用 archiver 或 zip-a-folder 通常能获得更好的维护和文档支持。

yazl的README

yazl

yet another zip library for node. For unzipping, see yauzl.

Design principles:

  • Don't block the JavaScript thread. Use and provide async APIs.
  • Keep memory usage under control. Don't attempt to buffer entire files in RAM at once.
  • Prefer to open input files one at a time than all at once. This is slightly suboptimal for time performance, but avoids OS-imposed limits on the number of simultaneously open file handles.

Usage

var yazl = require("yazl");

var zipfile = new yazl.ZipFile();
zipfile.addFile("file1.txt", "file1.txt");
// (add only files, not directories)
zipfile.addFile("path/to/file.txt", "path/in/zipfile.txt");
// pipe() can be called any time after the constructor
zipfile.outputStream.pipe(fs.createWriteStream("output.zip")).on("close", function() {
  console.log("done");
});
// alternate apis for adding files:
zipfile.addBuffer(Buffer.from("hello"), "hello.txt");
zipfile.addReadStreamLazy("stdin.txt", cb => cb(null, process.stdin));
// call end() after all the files have been added
zipfile.end();

API

Class: ZipFile

new ZipFile()

No parameters. Nothing can go wrong.

addFile(realPath, metadataPath, [options])

Adds a file from the file system at realPath into the zipfile as metadataPath. Typically metadataPath would be calculated as path.relative(root, realPath). Unzip programs would extract the file from the zipfile as metadataPath. realPath is not stored in the zipfile.

A valid metadataPath must not be blank. If a metadataPath contains "\\" characters, they will be replaced by "/" characters. After this substitution, a valid metadataPath must not start with "/" or /[A-Za-z]:\//, and must not contain ".." path segments. File paths must not end with "/", but see addEmptyDirectory(). After UTF-8 encoding, metadataPath must be at most 0xffff bytes in length.

options may be omitted or null and has the following structure and default values:

{
  mtime: stats.mtime,
  mode: stats.mode,
  compress: true,
  compressionLevel: 6,
  forceZip64Format: false,
  forceDosTimestamp: false,
  fileComment: "", // or a UTF-8 Buffer
}

Use mtime and/or mode to override the values that would normally be obtained by the fs.Stats for the realPath. The mode is the unix permission bits and file type. The mtime and mode are stored in the zip file in the fields "last mod file time", "last mod file date", and "external file attributes". yazl does not store group and user ids in the zip file.

If compress is true, the file data will be deflated (compression method 8). If compress is false, the file data will be stored (compression method 0). If compressionLevel is specified, it will be passed to zlib. Specifying compressionLevel: 0 is equivalent to compress: false. If both compress and compressionLevel are given, asserts that they do not conflict, i.e. !!compress === !!compressionLevel.

If forceZip64Format is true, yazl will use ZIP64 format in this entry's Data Descriptor and Central Directory Record even if not needed (this may be useful for testing.). Otherwise, yazl will use ZIP64 format where necessary.

Since yazl version 3.3.0, yazl includes the Info-ZIP "universal timestamp" extended field (0x5455 aka "UT") to encode the mtime. The Info-ZIP timestamp is a more modern encoding for the mtime and is generally recommended. Set forceDosTimestamp to true to revert to the pre-3.3.0 behvior, disabling this extended field. The DOS encoding is always included regardless of this option, because it is required in the fixed-size metadata of every archive entry. The benefits of the Info-ZIP encoding include: timezone is specified as always UTC, which is better for cloud environments and any teams working in multiple timezones; capable of encoding "time 0", the unix epoch in 1970, which is better for some package managers; the precision is 1-second accurate rather than rounded to the nearest even second. The disadvantages of including this field are: it requires an extra 9 bytes of metadata per entry added to the archive.

When attempting to encode an mtime outside the supported range for either format, such as the year 1970 in the DOS format or the year 2039 for the modern format, the time will clamped to the closest supported time.

If fileComment is a string, it will be encoded with UTF-8. If fileComment is a Buffer, it should be a UTF-8 encoded string. In UTF-8, fileComment must be at most 0xffff bytes in length. This becomes the "file comment" field in this entry's central directory file header.

Internally, fs.stat() is called immediately in the addFile function, and fs.createReadStream() is used later when the file data is actually required. Throughout adding and encoding n files with addFile(), the number of simultaneous open files is O(1), probably just 1 at a time.

addReadStream(readStream, metadataPath, [options])

Adds a file to the zip file whose content is read from readStream. This method is effectively implemented as this.addReadStreamLazy(metadataPath, options, cb => cb(null, readStream)).

In general, it is recommended to use addReadStreamLazy instead of this method to avoid holding a large number of system resources open for a long time. This method is provided for backward compatibility, and for convenience in cases where the readStream doesn't require meaningful resources to hold open and waiting.

addReadStreamLazy(metadataPath[, options], getReadStreamFunction)

Adds a file to the zip file whose content is read from a read stream obtained by calling getReadStreamFunction(cb). getReadStreamFunction(cb) is called with a single callback function. Your implementation of getReadStreamFunction should eventually call cb(err, readStream) and give the readStream that provides the contents of the file to add to the zip file. If err is given (if it is truthy), it will be emitted from this ZipFile object. The return value from cb is unspecified.

See addFile() for the meaning of the metadataPath parameter. typeof getReadStreamFunction must be 'function', which is used to determine when options has been omitted. options may be omitted or null and has the following structure and default values:

{
  mtime: new Date(),
  mode: 0o100664,
  compress: true,
  compressionLevel: 6,
  forceZip64Format: false,
  forceDosTimestamp: false,
  fileComment: "", // or a UTF-8 Buffer
  size: 12345, // example value
}

See addFile() for the meaning of mtime, mode, compress, compressionLevel, forceZip64Format, forceDosTimestamp, and fileComment. If size is given, it will be checked against the actual number of bytes in the readStream, and an error will be emitted if there is a mismatch. See the documentation on calculatedTotalSizeCallback for why the size option exists.

Note that yazl will .pipe() data from readStream, so be careful using .on('data'). In certain versions of node, .on('data') makes .pipe() behave incorrectly.

Here's an example call to this method to illustrate the function callbacks:

zipfile.addReadStreamLazy("path/in/archive.txt", function(cb) {
  var readStream = getTheReadStreamSomehow();
  cb(readStream);
});

addBuffer(buffer, metadataPath, [options])

Adds a file to the zip file whose content is buffer. See below for info on the limitations on the size of buffer. See addFile() for info about the metadataPath parameter. options may be omitted or null and has the following structure and default values:

{
  mtime: new Date(),
  mode: 0o100664,
  compress: true,
  compressionLevel: 6,
  forceZip64Format: false,
  forceDosTimestamp: false,
  fileComment: "", // or a UTF-8 Buffer
}

See addFile() for the meaning of mtime, mode, compress, compressionLevel, forceZip64Format, forceDosTimestamp, and fileComment.

This method has the unique property that General Purpose Bit 3 will not be used in the Local File Header. This doesn't matter for unzip implementations that conform to the Zip File Spec. However, 7-Zip 9.20 has a known bug where General Purpose Bit 3 is declared an unsupported compression method (note that it really has nothing to do with the compression method.). See issue #11. If you would like to create zip files that 7-Zip 9.20 can understand, you must use addBuffer() instead of addFile(), addReadStream(), or addReadStreamLazy() for all entries in the zip file (and addEmptyDirectory() is fine too).

Note that even when yazl provides the file sizes in the Local File Header, yazl never uses ZIP64 format for Local File Headers due to the size limit on buffer (see below).

Size limitation on buffer

In order to require the ZIP64 format for a local file header, the provided buffer parameter would need to exceed 0xfffffffe in length. Alternatively, the buffer parameter might not exceed 0xfffffffe in length, but zlib compression fails to compress the buffer and actually inflates the data to more than 0xfffffffe in length. Both of these scenarios are not allowed by yazl, and those are enforced by a size limit on the buffer parameter.

According to this zlib documentation, the worst case compression results in "an expansion of at most 13.5%, plus eleven bytes". Furthermore, some configurations of Node.js impose a size limit of 0x3fffffff on every Buffer object. Running this size through the worst case compression of zlib still produces a size less than 0xfffffffe bytes,

Therefore, yazl enforces that the provided buffer parameter must be at most 0x3fffffff bytes long.

addEmptyDirectory(metadataPath, [options])

Adds an entry to the zip file that indicates a directory should be created, even if no other items in the zip file are contained in the directory. This method is only required if the zip file is intended to contain an empty directory.

See addFile() for info about the metadataPath parameter. If metadataPath does not end with a "/", a "/" will be appended.

options may be omitted or null and has the following structure and default values:

{
  mtime: new Date(),
  mode: 040775,
  forceDosTimestamp: false,
}

See addFile() for the meaning of mtime, mode, and forceDosTimestamp.

end([options], [calculatedTotalSizeCallback])

Indicates that no more files will be added via addFile(), addReadStream(), addReadStreamLazy(), addBuffer(), or addEmptyDirectory(), and causes the eventual close of outputStream.

options may be omitted or null and has the following structure and default values:

{
  forceZip64Format: false,
  comment: "", // or a CP437 Buffer
}

If forceZip64Format is true, yazl will include the ZIP64 End of Central Directory Locator and ZIP64 End of Central Directory Record even if not needed (this may be useful for testing.). Otherwise, yazl will include these structures if necessary.

If comment is a string, it will be encoded with CP437. If comment is a Buffer, it should be a CP437 encoded string. comment must be at most 0xffff bytes in length and must not include the byte sequence [0x50,0x4b,0x05,0x06]. This becomes the ".ZIP file comment" field in the end of central directory record. Note that in practice, most zipfile readers interpret this field in UTF-8 instead of CP437. If your string uses only codepoints in the range 0x20...0x7e (printable ASCII, no whitespace except for single space ' '), then UTF-8 and CP437 (and ASCII) encodings are all identical. This restriction is recommended for maxium compatibility. To use UTF-8 encoding at your own risk, pass a Buffer into this function; it will not be validated.

If specified and non-null, calculatedTotalSizeCallback is given the parameters (calculatedTotalSize) sometime during or after the call to end(). calculatedTotalSize is of type Number and can either be -1 or the guaranteed eventual size in bytes of the output data that can be read from outputStream.

Note that calculatedTotalSizeCallback is usually called well before outputStream has piped all its data; this callback does not mean that the stream is done.

(The calculatedTotalSizeCallback feature was added to this library to support the use case of a web server constructing a zip file dynamically and serving it without buffering the contents on disk or in ram. calculatedTotalSize can become the Content-Length header before piping the outputStream as the response body.)

If calculatedTotalSize is -1, it means means the total size is too hard to guess before processing the input file data. To ensure the final size is known, disable compression (set compress: false or compressionLevel: 0) in every call to addFile(), addReadStream(), addReadStreamLazy(), and addBuffer(), and additionally specify the optional size option in every call to addReadStream() and addReadStreamLazy().

The call to calculatedTotalSizeCallback might be delayed if yazl is still waiting for fs.Stats for an addFile() entry. If addFile() was never called, calculatedTotalSizeCallback will be called during the call to end(). It is not required to start piping data from outputStream before calculatedTotalSizeCallback is called. calculatedTotalSizeCallback will be called only once, and only if this is the first call to end().

outputStream

A readable stream that will produce the contents of the zip file. It is typical to pipe this stream to a writable stream created from fs.createWriteStream().

Internally, large amounts of file data are piped to outputStream using pipe(), which means throttling happens appropriately when this stream is piped to a slow destination.

Data becomes available in this stream soon after calling one of addFile(), addReadStream(), addReadStreamLazy(), addBuffer(), or addEmptyDirectory(). Clients can call pipe() on this stream at any time, such as immediately after getting a new ZipFile instance, or long after calling end().

This stream will remain open while you add entries until you end() the zip file.

As a reminder, be careful using both .on('data') and .pipe() with this stream. In certain versions of node, you cannot use both .on('data') and .pipe() successfully.

dateToDosDateTime(jsDate)

Deprecated since yazl 3.3.0.

This function only remains exported in order to maintain compatibility with older versions of yazl. It will be removed in yazl 4.0.0 unless someone asks for it to remain supported. If you ever have a use case for calling this function directly please open an issue against yazl requesting that this function be properly supported again.

Regarding ZIP64 Support

yazl automatically uses ZIP64 format to support files and archives over 2^32 - 2 bytes (~4GB) in size and to support archives with more than 2^16 - 2 (65534) files. (See the forceZip64Format option in the API above for more control over this behavior.) ZIP64 format is necessary to exceed the limits inherent in the original zip file format.

ZIP64 format is supported by most popular zipfile readers, but not by all of them. Notably, the Mac Archive Utility does not understand ZIP64 format (as of writing this), and will behave very strangely when presented with such an archive.

Output Structure

The Zip File Spec leaves a lot of flexibility up to the zip file creator. This section explains and justifies yazl's interpretation and decisions regarding this flexibility.

This section is probably not useful to yazl clients, but may be interesting to unzip implementors and zip file enthusiasts.

Disk Numbers

All values related to disk numbers are 0, because yazl has no multi-disk archive support. (The exception being the Total Number of Disks field in the ZIP64 End of Central Directory Locator, which is always 1.)

Version Made By

Always 0x033f == (3 << 8) | 63, which means UNIX (3) and made from the spec version 6.3 (63).

Note that the "UNIX" has implications in the External File Attributes.

Version Needed to Extract

Usually 20, meaning 2.0. This allows filenames and file comments to be UTF-8 encoded.

When ZIP64 format is used, some of the Version Needed to Extract values will be 45, meaning 4.5. When this happens, there may be a mix of 20 and 45 values throughout the zipfile.

General Purpose Bit Flag

Bit 11 is always set. Filenames (and file comments) are always encoded in UTF-8, even if the result is indistinguishable from ascii.

Bit 3 is usually set in the Local File Header. To support both a streaming input and streaming output api, it is impossible to know the crc32 before processing the file data. When bit 3 is set, data Descriptors are given after each file data with this information, as per the spec. But remember a complete metadata listing is still always available in the central directory record, so if unzip implementations are relying on that, like they should, none of this paragraph will matter. Even so, some popular unzip implementations do not follow the spec. The Mac Archive Utility requires Data Descriptors to include the optional signature, so yazl includes the optional data descriptor signature. When bit 3 is not used, the Mac Archive Utility requires there to be no data descriptor, so yazl skips it in that case. Additionally, 7-Zip 9.20 does not seem to support bit 3 at all (see issue #11).

All other bits are unset.

Internal File Attributes

Always 0. The "apparently an ASCII or text file" bit is always unset meaning "apparently binary". This kind of determination is outside the scope of yazl, and is probably not significant in any modern unzip implementation.

External File Attributes

Always stats.mode << 16. This is apparently the convention for "version made by" = 0x03xx (UNIX).

Note that for directory entries (see addEmptyDirectory()), it is conventional to use the lower 8 bits for the MS-DOS directory attribute byte. However, the spec says this is only required if the Version Made By is DOS, so this library does not do that.

Directory Entries

When adding a metadataPath such as "parent/file.txt", yazl does not add a directory entry for "parent/", because file entries imply the need for their parent directories. Unzip clients seem to respect this style of pathing, and the zip file spec does not specify what is standard in this regard.

In order to create empty directories, use addEmptyDirectory().

Size of Local File and Central Directory Entry Metadata

The spec recommends that "The combined length of any directory record and [the file name, extra field, and comment fields] should not generally exceed 65,535 bytes". yazl makes no attempt to respect this recommendation. Instead, each of the fields is limited to 65,535 bytes due to the length of each being encoded as an unsigned 16 bit integer.

Change History

  • 3.3.1 (2024-Nov-23)
    • Fix bug in addReadStreamLazy() timing that could result in the given getReadStreamFunction being called too soon or being called more than once. issue #87
  • 3.3.0 (2024-Nov-08)
    • Add support for encoding timestamps in the more modern Info-ZIP "universal timestamp" extended field (0x5455 aka "UT"): supports years as old as 1901 instead of only 1980, notably including 1970; encodes timestamp in UTC rather than an unspecified system-dependent local timezone. pull #86
    • Disable spending the extra 9 bytes of metadata per entry with forceDosTimestamp:true.
    • Out-of-bounds timestamps are now clamped rather than overflowing/underflowing and wrapping around.
    • Marked dateToDosDateTime() as deprecated.
  • 3.2.1 (2024-Nov-03)
    • Fix typo in addBuffer() where compressionLevel wasn't being passed to zlib.
  • 3.2.0 (2024-Nov-02)
    • Add compressionLevel option to addFile(), addBuffer(), addReadStream(), addReadStreamLazy(). issue #84
    • Change wording around calculatedTotalSizeCallback again, and fix the documentation incorrectly claiming that addEmptyDirectory() was relevant in that context.
  • 3.1.0 (2024-Oct-19)
    • Add addReadStreamLazy() as a replacement for addReadStream(). The latter is maintained for compatibility and situational convenience. issue #74 pull #80
    • The add*() methods will now throw an error if you call them after calling end(). The documented semantics have always regarded this as undefined behavior.
    • Some subtle and undocumented changes to error handling. Error handling isn't very well tested.
  • 3.0.1 (2024-Oct-19)
    • Change the documented name of finalSizeCallback to calculatedTotalSizeCallback to hopefully avoid confusion. issue #71
  • 3.0.0 (2024-Oct-18)
    • Upgrade dependencies. This indirectly drops support for older versions of node.
  • 2.5.1 (2018-Nov-30)
    • Fix support for old versions of Node and add official support for Node versions 0.10, 4, 6, 8, 10. pull #49
  • 2.5.0 (2018-Nov-15)
    • Add support for comment and fileComment. pull #44
    • Avoid new Buffer(). pull #43
  • 2.4.3 (2017-Oct-30)
  • 2.4.2 (2016-Oct-26)
    • Remove octal literals to make yazl compatible with strict mode. pull #28
  • 2.4.1 (2016-Jun-30)
    • Fix Mac Archive Utility compatibility issue. issue #24
  • 2.4.0 (2016-Jun-26)
  • 2.3.1 (2016-May-09)
    • Remove .npmignore from npm package. pull #22
  • 2.3.0 (2015-Dec-23)
    • metadataPath can have \ characters now; they will be replaced with /. issue #18
  • 2.2.2 (2015-May-07)
  • 2.2.1 (2015-Mar-27)
    • Fix Mac Archive Utility compatibility issue. issue #14
  • 2.2.0 (2015-Mar-21)
    • Avoid using general purpose bit 3 for addBuffer() calls. issue #13
  • 2.1.3 (2015-Mar-20)
    • Fix bug when only addBuffer() and end() are called. issue #12
  • 2.1.2 (2015-Mar-03)
  • 2.1.1 (2015-Mar-01)
    • Fixed stack overflow when using addBuffer() in certain ways. issue #9
  • 2.1.0 (2015-Jan-16)
    • Added addEmptyDirectory().
    • options is now optional for addReadStream() and addBuffer().
  • 2.0.0 (2014-Sep-24)
    • Initial release.