ZipArchive 是比 ZipFile 更底层、可控的选择,适合自定义路径、保留时间戳、处理符号链接或增量更新;需手动递归遍历目录、显式创建空目录条目、校验解压路径防穿越、注意 Update 模式限制及流正确处置。

ZipArchive 是比 ZipFile 更底层、更可控的选择,适合需要自定义路径、保留时间戳、处理符号链接或增量更新 ZIP 的场景。直接用 ZipFile.CreateFromDirectory 看似省事,但一遇到中文路径、空目录、上级跳转或时间戳丢失,就容易出问题。
用 ZipArchive 打包整个文件夹(含子目录和空目录)
必须手动递归遍历,不能依赖 ZipFile.CreateFromDirectory 的“自动扁平化”逻辑——它会跳过空目录,且无法控制条目名前缀。
- 对每个
FileInfo调用archive.CreateEntryFromFile(),传入相对路径(如"docs/readme.txt"),别传绝对路径 - 对每个
DirectoryInfo,若其下无文件但需保留目录结构,得手动调用archive.CreateEntry("path/to/dir/")(结尾带/) - 用
Path.GetRelativePath(rootDir, file.FullName)生成干净的内部路径,避免..\或盘符 - 记得设
entry.LastWriteTime = file.LastWriteTime,否则所有文件在 ZIP 中显示为打包时刻
解压时防止路径穿越(../../../etc/passwd 类攻击)
ZipArchive.ExtractToDirectory 不校验路径,直接调用等于把 ZIP 当作可信输入——生产环境绝不能这么干。
- 遍历
archive.Entries,对每个entry.FullName做双重检查:
— 是否以".."开头或含"../"(entry.FullName.StartsWith("..") || entry.FullName.Contains("../"))
— 是否为绝对路径(Path.IsPathRooted(entry.FullName)) - 再用
Path.GetFullPath(Path.Combine(targetDir, entry.FullName))和targetDir比较前缀,确保解压目标落在预期目录内 - 跳过非法条目,或抛出
SecurityException,不要静默忽略
ZipArchiveMode.Update 添加文件到已有 ZIP 的坑
想往一个已存在的 .zip 文件里追加新文件?ZipArchiveMode.Update 看似可行,但有硬限制:
- 源 ZIP 文件必须用
FileMode.Open+FileAccess.ReadWrite打开,不能是FileMode.Create - 不支持删除已有条目,只支持新增或覆盖(覆盖需先
archive.GetEntry(name)再entry.Delete(),但该方法仅 .NET 6+ 支持) - 如果 ZIP 已被其他进程锁定(如资源管理器预览缩略图),
FileStream构造就会失败,报IOException - 写完必须显式调用
archive.Dispose(),否则 ZIP 结构损坏,WinRAR 打开提示InvalidDataException: End of Central Directory record could not be found
压缩单个文件却不建临时目录的写法
ZipFile.CreateFromDirectory 强制要求源是目录,但你只想压一个 report.pdf?不用临时文件夹也能做。
- 用
FileStream创建 ZIP 文件,传给ZipArchive构造函数,模式设为ZipArchiveMode.Create - 调用
archive.CreateEntry("report.pdf"),再用entry.Open()得到可写流 - 用
File.OpenRead("report.pdf").CopyTo(entryStream)流式写入,避免大文件爆内存 - 务必等
entryStream关闭、archive.Dispose()完成后再释放FileStream
真正麻烦的从来不是“怎么写完”,而是“怎么写对”:路径校验漏一条、Dispose 忘一次、时间戳没同步、空目录没显式创建——这些细节在本地测试都正常,上线后才暴露。建议把路径校验和流处置逻辑封装成可复用的 helper 方法,别每次手写。


















