PHP直接解压php://input的ZIP数据流?终极解决方案
2025-03-18 16:08:46
直接解压 php://input 中的 ZIP 文件?搞不定!
最近遇到个棘手的问题,想把通过 php://input
接收到的 ZIP 压缩包直接解压,不经过临时文件,省点事儿。 尝试了好几种方法, 都没成功,很郁闷!
为什么直接从 php://input
解压这么难?
先说说背景,我用的是 Laravel Homestead 环境,PHP 版本是 7.1.3。 我用 curl
命令向 myproject.app/upload
这个接口发送 POST 请求,并把 ZIP 文件作为二进制数据传过去:
curl --request POST \
--url 'http://myproject.app/upload' \
--data-binary "@myfile.zip" \
问题就出在, PHP 的内置 ZIP 处理函数, 好像不太擅长直接处理流数据。 正常来说,ZipArchive
类应该可以打开文件、流等资源, 但直接用 php://input
就会报错。 猜测可能是因为php://input
这个流是只读的、一次性的, ZipArchive
内部需要对文件进行多次读取或者寻址之类的操作, 而 php://input
这种流不支持这些。
compress.zlib://php://input
这个方式呢, 则是期望输入流本身已经是一个 zlib 压缩的数据流(例如 .gz 文件),而不是 zip 格式的数据流。因为这层封装只处理解压,不处理zip文件的格式。
stream_filter_append
也类似, 他只是添加了一层 filter 来解压数据流, 并不能正确识别 ZIP 文件格式,并对其解包。
几个失败的尝试和报错信息
我尝试了好几种方法,都以失败告终。 下面列出来,也给大伙儿提个醒:
方法一: 使用 compress.zlib://
封装
dd(file_get_contents('compress.zlib://php://input'));
报错:
file_get_contents(): cannot represent a stream of type Input as a File Descriptor
方法二: 使用流过滤器 zlib.inflate
$fh = fopen('php://input', 'rb');
stream_filter_append($fh, 'zlib.inflate', STREAM_FILTER_READ, array('window'=>15));
$data = '';
while (!feof($fh)) {
$data .= fread($fh, 8192);
}
dd($data);
结果:
"" //空字符串
方法三: 使用 ZipArchive
类
$zip = new ZipArchive;
$zip->open('php://input');
$zip->extractTo(storage_path() . '/' . 'myfile');
$zip->close();
报错:
ZipArchive::extractTo(): Invalid or uninitialized Zip object
可行的解决办法:还是要写入临时文件
虽然很不想承认,但经过一番折腾,我发现直接从 php://input
解压 ZIP 文件这条路,目前来说可能真的走不通。 PHP 的 ZipArchive
可能压根就没设计成这样用。 没办法,只能曲线救国,先写入临时文件,再操作。
方案: 使用临时文件
-
原理: 先把
php://input
的内容保存到一个临时文件中,然后使用ZipArchive
类打开并解压这个临时文件。 操作完毕后,删除临时文件。 -
步骤:
- 生成一个唯一的临时文件名。
- 从
php://input
读取数据并写入到临时文件中。 - 使用
ZipArchive
解压临时文件。 - 删除临时文件.
-
代码示例:
// 生成唯一临时文件名
$tempFilePath = tempnam(sys_get_temp_dir(), 'zip');
// 将 php://input 的内容写入临时文件
$input = fopen('php://input', 'rb');
$tempFile = fopen($tempFilePath, 'wb');
stream_copy_to_stream($input, $tempFile);
fclose($input);
fclose($tempFile);
// 使用 ZipArchive 解压临时文件
$zip = new ZipArchive;
if ($zip->open($tempFilePath) === TRUE) {
$zip->extractTo(storage_path() . '/' . 'myfile');
$zip->close();
// 解压成功,删除临时文件
unlink($tempFilePath);
echo "解压成功!";
} else {
// 解压失败,也删除临时文件
unlink($tempFilePath);
echo "解压失败!";
}
- 安全提示 :
- 确保你的临时文件目录(
sys_get_temp_dir()
返回的)有适当的权限,PHP进程要有写入权限. - 解压操作最好有异常捕获, 保证发生任何问题都能删掉临时文件.
进阶方案(如果实在不想用临时文件): 使用 proc_open 调用外部 unzip 命令
-
原理: 如果你的服务器环境允许, 并且安装了
unzip
命令行工具,你可以使用 PHP 的proc_open
函数来直接处理输入流. 这种方式有点 Hack, 但是如果对性能要求特别高, 并且确定服务器有unzip
工具, 可以考虑尝试一下。 -
步骤:
- 构建
unzip
命令,将标准输入作为压缩文件源。 - 使用
proc_open
打开这个命令进程。 - 将
php://input
的数据通过管道输送到unzip
的标准输入. - 从
unzip
的标准输出或标准错误读取输出。
- 构建
-
代码示例 (注意,需要服务器安装 unzip 命令行工具):
$descriptorspec = array(
0 => array("pipe", "r"), // stdin 是一个管道,子进程从这里读取
1 => array("pipe", "w"), // stdout 是一个管道,子进程向这里写入
2 => array("pipe", "w") // stderr 是一个管道, 子进程错误信息写入
);
$process = proc_open('unzip -d ' . escapeshellarg(storage_path() . '/' . 'myfile') . ' -', $descriptorspec, $pipes);
if (is_resource($process)) {
// $pipes 现在看起来是这样的:
// 0 => 可以向子进程 stdin 写入的句柄
// 1 => 可以从子进程 stdout 读取的句柄
// 2 => 可以从子进程 stderr 读取的句柄
$input = fopen('php://input', 'rb');
stream_copy_to_stream($input, $pipes[0]); //把php://input内容写入到unzip的标准输入.
fclose($input);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// 获取返回状态,很重要的。
$return_value = proc_close($process);
echo "stdout: " . $stdout . "\n";
echo "stderr: " . $stderr . "\n";
echo "command returned: " . $return_value . "\n";
}
- 安全提示和进阶使用:
escapeshellarg
务必用来转义输出路径, 避免命令注入风险!- 对
unzip
命令的输出和返回码 ($return_value
) 进行仔细检查, 确保解压过程正确执行,没有出错. - 通过捕获
stderr
输出, 可以更详细地了解 unzip 过程中是否有问题。 - 可以通过调整
unzip
的参数,进一步控制解压行为 (例如-o
覆盖已存在文件,-t
测试压缩包完整性等)。 - 这种方法对系统环境有依赖,如果
unzip
命令不可用,则此方法行不通,请确保已经安装unzip
。
总结
看来,直接在 PHP 里操作 php://input
的 ZIP 流是不太现实的. 如果你也有类似需求,用临时文件的方法应该是最稳妥的。 如果真的有很高性能需求,并且服务器环境允许, 可以考虑使用 proc_open
的方案.