本章学习 yauzl 的入口 API、常用配置和
ZipFile。示例使用 TypeScript,并以当前项目“上传 ZIP 保存到临时文件”为主要场景。
运行时依赖和类型声明是两件事:
npm install yauzl
npm install --save-dev @types/yauzl
yauzl:程序运行时真正执行的 JavaScript;@types/yauzl:TypeScript 类型检查使用,生产运行时通常不需要。yauzl 使用传统 CommonJS 导出。当前项目编译目标是 CommonJS,可以使用:
import * as yauzl from "yauzl";
如果工程开启了 esModuleInterop,也可能看到默认导入写法:
import yauzl from "yauzl";
不要为了复制某一种导入写法而盲目修改整个工程的模块配置,应以当前 tsconfig.json 和类型检查结果为准。
| API | 输入 | 适合场景 |
|---|---|---|
open() |
文件路径 | Multer 已将 ZIP 保存到磁盘 |
fromBuffer() |
Buffer |
很小且已有严格限制的内存上传 |
fromFd() |
文件描述符 | 调用方已经打开文件并管理 fd |
fromRandomAccessReader() |
自定义随机读取器 | ZIP 位于特殊存储或远程介质 |
新人在当前功能中重点掌握前两个即可。
open() 打开磁盘文件yauzl 的原始 API 使用 Node.js 风格 callback:
import * as yauzl from "yauzl";
yauzl.open(
"./temporary/upload.zip",
{
lazyEntries: true,
autoClose: true,
decodeStrings: true,
validateEntrySizes: true,
strictFileNames: true,
},
(error, zipFile) => {
if (error) {
console.error("无法打开 ZIP", error);
return;
}
console.log("Entry 数量:", zipFile.entryCount);
zipFile.readEntry();
},
);
“打开成功”只说明 yauzl 已识别 ZIP 结构并创建 ZipFile。文件正文仍未全部解压。
open() 可能因为以下原因失败:
不能仅通过 .zip 扩展名判断它是合法 ZIP。
lazyEntrieslazyEntries: true
默认值是 false。默认模式会持续产生 entry 事件;开启后,只有调用:
zipFile.readEntry();
才会读取下一个 Entry。
推荐在服务端处理上传 ZIP 时开启,因为它让我们形成严格的顺序:
readEntry()
→ 校验当前 Entry
→ 读取或跳过当前 Entry
→ 当前项处理完成
→ 再次 readEntry()
这样更容易限制资源,也不会一口气启动大量异步任务。
autoCloseautoClose: true
使用 open() 时默认是 true。当 yauzl 正常读到 Central Directory 末尾后,会自动关闭底层文件资源。
但是 autoClose 不代表所有失败路径都无需处理。如果业务在中途拒绝一个 Entry,或者客户端已经断开,应该主动停止流程并调用:
zipFile.close();
同时仍要销毁正在读取或写入的 Stream。
decodeStringsdecodeStrings: true
默认值是 true。yauzl 会根据 ZIP 文件名相关标志,将文件名解码成字符串,日常业务一般保持默认值。
设置为 false 时,文件名及注释可能以原始 Buffer 形式提供,适合需要自行处理特殊编码的底层工具。这个模式会让 TypeScript 和路径检查都更复杂,不建议新人在普通上传接口中使用。
一些类型声明版本可能把
entry.fileName固定标为string,但关闭字符串解码后运行时语义会变化。若确实使用该高级配置,必须同时核对当前 yauzl 版本和类型声明,不要只相信编辑器提示。
validateEntrySizesvalidateEntrySizes: true
默认值是 true。它帮助 yauzl 检查 Entry 声明的大小与实际解压数据是否一致:
保持开启有助于识别损坏或恶意构造的数据,但它不是业务大小限制。
下面仍然是必要的:
if (entry.uncompressedSize > MAX_SINGLE_FILE_SIZE) {
throw new Error("单个文件解压后过大");
}
还需要限制所有 Entry 的累计大小和 Entry 数量。
strictFileNamesstrictFileNames: true
默认值是 false。ZIP 规范使用 / 作为路径分隔符,但现实中有些工具会写入 Windows 反斜杠 \。
false:yauzl 为兼容旧 ZIP,可能把反斜杠规范化为 /;true:按更严格的文件名规则处理,遇到不规范名称时失败。接收不可信上传时,可以开启严格模式,减少不同操作系统对路径解释不一致的问题。
不过它仍然不能代替应用自己的目标路径检查。应用必须确认最终写入路径位于临时根目录之内。
open()在 async/await 项目中,可以只在适配层包装一次 callback:
import * as yauzl from "yauzl";
const ZIP_OPTIONS: yauzl.Options = {
lazyEntries: true,
autoClose: true,
decodeStrings: true,
validateEntrySizes: true,
strictFileNames: true,
};
export function openZipFile(
zipPath: string,
): Promise<yauzl.ZipFile> {
return new Promise((resolve, reject) => {
yauzl.open(zipPath, ZIP_OPTIONS, (error, zipFile) => {
if (error) {
reject(error);
return;
}
resolve(zipFile);
});
});
}
使用方式:
const zipFile = await openZipFile(
"./temporary/upload.zip",
);
这里的 Promise 只覆盖“打开 ZIP”这一步。打开完成之后,ZipFile 仍会通过 error 事件报告遍历或读取阶段的错误,所以后续代码仍必须注册事件监听器。
不要误以为:
const zipFile = await openZipFile(path);
执行成功就代表 ZIP 内所有文件均合法、均可正常解压。
fromBuffer()如果 Multer 使用 memoryStorage(),上传结果位于:
req.file.buffer
可以这样打开:
export function openZipBuffer(
buffer: Buffer,
): Promise<yauzl.ZipFile> {
return new Promise((resolve, reject) => {
yauzl.fromBuffer(
buffer,
{
lazyEntries: true,
decodeStrings: true,
validateEntrySizes: true,
strictFileNames: true,
},
(error, zipFile) => {
if (error) {
reject(error);
return;
}
resolve(zipFile);
},
);
});
}
fromBuffer() 的特点是:
因此只推荐在同时满足以下条件时使用:
例如,每个 ZIP 最多 20 MiB,并发 100 个请求,光上传 Buffer 理论上就可能接近 2 GiB,尚未计算解压和转换产生的数据。
ZipFile 的核心成员entryCountzipFile.entryCount
表示 Central Directory 中声明的 Entry 数量,可以做一次快速前置检查:
const MAX_ENTRY_COUNT = 500;
if (zipFile.entryCount > MAX_ENTRY_COUNT) {
zipFile.close();
throw new Error("ZIP 中的文件数量过多");
}
但后续遍历时仍应自己计数,不应把安全性只寄托在一个元数据字段上。
readEntry()仅当 lazyEntries: true 时使用:
zipFile.readEntry();
它请求 yauzl 读取下一个目录项。调用后,结果通过 entry、end 或 error 事件送达,不会直接作为返回值返回。
openReadStream()zipFile.openReadStream(entry, callback);
为某个文件 Entry 创建正文读取流。它也可能失败,例如压缩方法不支持或 ZIP 数据损坏。
close()zipFile.close();
主动关闭 ZipFile。中途拒绝、超时、客户端断开和业务取消时都可能需要它。
关闭 ZipFile 与清理已经写入的临时文件是两件事,仍要在业务层用 finally 删除工作目录。
ZipFile 的重要事件| 事件 | 含义 |
|---|---|
entry |
读到了一个目录项 |
end |
所有 Central Directory Entry 已读完 |
error |
解析或读取过程出错 |
close |
ZipFile 底层资源关闭 |
基本骨架:
const zipFile = await openZipFile(zipPath);
zipFile.on("entry", (entry) => {
console.log(entry.fileName);
zipFile.readEntry();
});
zipFile.once("error", (error) => {
console.error("读取 ZIP 失败", error);
});
zipFile.once("end", () => {
console.log("所有 Entry 已读取");
});
zipFile.once("close", () => {
console.log("ZipFile 已关闭");
});
zipFile.readEntry();
end 和 close 不完全等价:
end 关注 Entry 遍历已经结束;close 关注底层资源已经关闭。validateEntrySizes 就不会遇到 ZIP 炸弹它验证“声明是否匹配”,不限制声明本身有多大。一个合法声明解压后 50 GiB 的 Entry 仍然危险。
autoClose 会删除上传和解压文件它只负责关闭 yauzl 使用的底层资源,不会删除任何业务临时文件。
readEntry() 会返回 Entry它没有返回 Entry。结果由事件异步送达。
fromBuffer() 就是纯流式、低内存Entry 可以流式读取,但整个 ZIP 输入 Buffer 已经存在内存中。
strictFileNames 等于完整路径安全它只是库的一层文件名规则。最终目标路径是否逃出工作目录,必须由应用再次验证。
open() 与 fromBuffer() 分别适合什么输入?lazyEntries: true?autoClose 能否清理解压到磁盘的临时文件?为什么?validateEntrySizes 与应用设置的 maxSingleEntrySize 有什么区别?strictFileNames: true 为什么仍不能代替目标路径检查?open() 后,为什么还要监听 ZipFile 的 error 事件?fromBuffer() 时,应该怎样估算最坏并发内存占用?end 事件和 close 事件各自描述什么状态?