PHP使用ZipArchive生成Zip文件下载后无法解压排查
PHP ZipArchive生成压缩包无法解压问题修复
问题现象
通过PHP ZipArchive类编写代码生成并下载Zip压缩包,下载得到的文件无法被解压工具正常打开,手动创建的正常压缩包与异常压缩包差异对比如下:
原问题代码
$zip = new ZipArchive; $zip_name = 'test.zip'; if ($zip->open($zip_name, ZipArchive::CREATE) === TRUE) { $zip->addFile('1.csv', 'demofile/1.csv'); $zip->addFile('2.csv', 'demofile/2.csv'); $zip->addFile('1.csv', 'demofile2/1.json'); $zip->addFromString('demofile/1.csv', $content_1); $zip->addFromString('demofile/2.csv', $content_2); $zip->addFromString('demofile2/1.json', $contents); $zip->close(); header('Content-type: application/zip'); header('Content-Disposition: attachment; filename="'.$zip_name.'"'); header("Content-length: ".filesize($zip_name)); readfile($zip_name); exit(); }
故障原因
- 同路径重复写入导致压缩包结构损坏:代码先调用
addFile为3个包内路径添加本地文件,紧接着又调用addFromString给完全相同的3个路径写入字符串内容。ZipArchive不会自动覆盖同路径的旧条目,重复写入会直接破坏压缩包中央目录结构,解压工具无法正确解析文件索引。另外addFile('1.csv', 'demofile2/1.json')本身存在逻辑错误,是将csv格式文件存为json后缀,就算不重复写入也不符合预期。 - 输出流污染:代码没有在输出Zip文件前清空输出缓冲区,只要脚本在
readfile()执行前输出了任何内容(包括PHP标签外的空格、BOM头、Notice/Warning级报错),都会被拼接到Zip二进制流的开头,直接导致Zip文件头损坏。 - 临时文件读写冲突:代码将生成的Zip文件固定存为运行目录下的
test.zip,没有加文件锁也没有做唯一命名,高并发场景下会出现多个请求同时读写同一个文件的情况,可能读到写入不完整的损坏文件。 - 缺少缓存控制头:没有设置禁止缓存的响应头,浏览器或中间代理可能缓存之前损坏的压缩包,导致用户反复下载到错误文件。
修复方案
- 删除冗余的
addFile调用:既然已经通过addFromString传入了各文件的实际内容,不需要再调用addFile读取本地文件,从根源避免同路径重复写入问题。 - 打开Zip文件时增加
ZipArchive::OVERWRITE标记,确保每次生成都覆盖旧文件,不会残留旧文件的损坏内容。 - 在输出文件前调用缓冲区清理函数,清空所有提前输出的无关内容,避免二进制流被污染。
- 补充缓存控制响应头,禁用客户端和代理缓存。
- 文件输出完成后删除临时生成的Zip文件,避免服务器残留垃圾文件,也减少后续请求的读写冲突概率。
修复后可直接使用的代码
$zip = new ZipArchive; $zip_name = 'test.zip'; // 增加OVERWRITE标记,每次生成覆盖旧文件 if ($zip->open($zip_name, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) { // 移除冗余addFile调用,仅通过addFromString写入目标内容 $zip->addFromString('demofile/1.csv', $content_1); $zip->addFromString('demofile/2.csv', $content_2); $zip->addFromString('demofile2/1.json', $contents); $zip->close(); // 清空输出缓冲区,避免无关内容污染Zip流 ob_clean(); // 补充完整响应头 header('Content-type: application/zip'); header('Content-Disposition: attachment; filename="'.$zip_name.'"'); header("Content-length: ".filesize($zip_name)); header('Pragma: no-cache'); header('Expires: 0'); readfile($zip_name); // 删除临时文件 unlink($zip_name); exit(); }
内容的提问来源于stack exchange,提问作者kovrita is The life
相关产品推荐
相关产品推荐

