返回

PHP直接解压php://input的ZIP数据流?终极解决方案

php

直接解压 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 可能压根就没设计成这样用。 没办法,只能曲线救国,先写入临时文件,再操作。

方案: 使用临时文件

  1. 原理: 先把 php://input 的内容保存到一个临时文件中,然后使用 ZipArchive 类打开并解压这个临时文件。 操作完毕后,删除临时文件。

  2. 步骤:

    1. 生成一个唯一的临时文件名。
    2. php://input读取数据并写入到临时文件中。
    3. 使用 ZipArchive 解压临时文件。
    4. 删除临时文件.
  3. 代码示例:

// 生成唯一临时文件名
$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 "解压失败!";
}

  1. 安全提示 :
  • 确保你的临时文件目录(sys_get_temp_dir()返回的)有适当的权限,PHP进程要有写入权限.
  • 解压操作最好有异常捕获, 保证发生任何问题都能删掉临时文件.

进阶方案(如果实在不想用临时文件): 使用 proc_open 调用外部 unzip 命令

  1. 原理: 如果你的服务器环境允许, 并且安装了 unzip 命令行工具,你可以使用 PHP 的 proc_open 函数来直接处理输入流. 这种方式有点 Hack, 但是如果对性能要求特别高, 并且确定服务器有 unzip 工具, 可以考虑尝试一下。

  2. 步骤:

    1. 构建 unzip 命令,将标准输入作为压缩文件源。
    2. 使用 proc_open 打开这个命令进程。
    3. php://input的数据通过管道输送到unzip的标准输入.
    4. unzip 的标准输出或标准错误读取输出。
  3. 代码示例 (注意,需要服务器安装 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";
}
  1. 安全提示和进阶使用:
  • escapeshellarg 务必用来转义输出路径, 避免命令注入风险!
  • unzip 命令的输出和返回码 ($return_value) 进行仔细检查, 确保解压过程正确执行,没有出错.
  • 通过捕获 stderr 输出, 可以更详细地了解 unzip 过程中是否有问题。
  • 可以通过调整 unzip 的参数,进一步控制解压行为 (例如 -o 覆盖已存在文件, -t 测试压缩包完整性等)。
  • 这种方法对系统环境有依赖,如果unzip 命令不可用,则此方法行不通,请确保已经安装 unzip

总结

看来,直接在 PHP 里操作 php://input 的 ZIP 流是不太现实的. 如果你也有类似需求,用临时文件的方法应该是最稳妥的。 如果真的有很高性能需求,并且服务器环境允许, 可以考虑使用 proc_open 的方案.