system.io.compression 是最稳妥的 zip 压缩方案,需显式设 compressionlevel.optimal、用 ziparchivemode.create、正确处置流;解压须校验路径防穿越,压缩需处理中文编码、时间戳和符号链接。

用 System.IO.Compression 压缩 ZIP 文件最稳妥
.NET Framework 4.5+ 和 .NET Core 2.0+ 内置的 System.IO.Compression 是首选,无需第三方包,不依赖外部工具,也不会因权限或路径空格出错。
关键点:必须显式指定 CompressionLevel.Optimal(默认是 Fastest,压缩率极低),且文件路径要用 ZipArchiveMode.Create 打开归档,不能直接写入文件流后就关闭——否则 ZIP 结构损坏,WinRAR 或系统自带解压器打不开。
常见错误现象:InvalidDataException: End of Central Directory record could not be found,基本就是没正确调用 archive.Dispose() 或没等所有 ZipArchiveEntry 写完就关流。
- 源文件路径含中文?没问题,但确保
FileStream用FileMode.Create+FileAccess.Write,别用FileShare.Read - 要压缩整个文件夹?得自己递归遍历,
ZipFile.CreateFromDirectory虽方便,但不支持自定义条目名(比如去掉父路径前缀) - 大文件(>1GB)?避免一次性读入内存,用
entry.Open().CopyTo(stream)流式处理
解压 ZIP 时如何避免路径穿越漏洞
ZipArchive.ExtractToDirectory 默认不做路径校验,恶意 ZIP 包里塞个 ../../../etc/passwd 条目,一解压就可能覆盖系统文件。生产环境必须手动校验每个 ZipArchiveEntry.FullName。
核心判断逻辑:用 Path.GetRelativePath(.NET Core 2.1+)或正则 ^\.\./ 检查是否含上级目录跳转;再确认 entry.FullName 不以 / 或 \ 开头,也不含控制字符。
- 旧版 .NET Framework?用
entry.FullName.StartsWith("..") || entry.FullName.Contains("../")粗筛,再用Path.IsPathRooted排除绝对路径 - 解压到临时目录?先
Path.GetTempPath()创建唯一子目录,再把校验通过的条目ExtractToFile到该目录下 - 遇到空目录条目(
entry.FullName.EndsWith("/"))?跳过或用Directory.CreateDirectory单独建
ZipFile.CreateFromDirectory 的隐藏坑:时间戳和符号链接
这个快捷方法看似省事,但有两个硬伤:一是所有文件在 ZIP 中的时间戳统一为打包时刻(丢失原始 LastWriteTime),二是完全忽略符号链接(Linux/macOS 下的 symlink 直接被跳过,不会报错)。
如果你需要保留原始修改时间,或者处理跨平台项目(比如 CI 构建时打包含 symlink 的 Node.js 依赖),必须绕开 ZipFile,改用 ZipArchive 手动添加条目,并设置 entry.LastWriteTime 属性。
- 设置时间戳:获取源文件
File.GetLastWriteTimeUtc(path),赋值给entry.LastWriteTime - 符号链接处理:在 Linux 上用
File.GetAttributes(path)判断FileAttributes.ReparsePoint,然后读取目标路径并存为普通文件(ZIP 标准本身不支持 symlink) - 压缩级别无效?对,
ZipFile.CreateFromDirectory不暴露CompressionLevel参数,只能用底层ZipArchive控制
遇到 NotSupportedException: No data is available for encoding 936 怎么办
这是 ZIP 中文路径在非中文系统(如英文 Windows Server、Docker 容器)上解压失败的经典报错,根源是 ZIP 规范早期未强制要求 UTF-8 编码,而 .NET 默认用系统 OEM 编码(GBK=936)解析文件名。
解决方案分两层:压缩时主动声明 UTF-8(需 .NET 6+),解压时强制用 UTF-8 解析(兼容旧版)。前者靠 ZipArchiveMode.Create 构造时传 new ZipArchiveOptions { Encoding = Encoding.UTF8 };后者需手动读取 ZipArchiveEntry 的原始字节再解码。
- .NET 5 及以下?无法设置编码参数,只能解压时用反射获取
_name字段的原始 byte[],再Encoding.UTF8.GetString(bytes) - Docker 部署?在
Dockerfile加ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false,否则连 UTF-8 编码器都不可用 - 别信“改系统区域设置”这种方案,容器里不生效,且影响其他应用
路径校验、编码处理、时间戳控制这三块最容易在线上环境突然出问题,尤其当 ZIP 来源不可控或部署环境异构时。宁可多写几行校验逻辑,也不要依赖框架默认行为。










