将分块 gzip 文件写入 PHP 中的任意输出流

Writing a chunked gzip file to an arbitrary output stream in PHP

我的原始代码是这样做的:

    $data = file_get_contents($source);
    $compressed_data = gzencode($data);
    file_put_contents($destination, $compressed_data);

这很好用,它似乎支持 $source$destination 的许多不同值 - 包括 in-memory 文件系统、stdin/stdout 流等。

但是,大文件必须完全加载到内存中,所以我想将其切换为分块方法。

我试过以下方法:

    $in = fopen($source, 'rb');
    $out = gzopen($destination, 'wb');
    while (!feof($in)) {
        gzwrite($out, fread($in, 4096));
    }

但这给了我一个流包装器错误(例如 https://packagist.org/packages/mikey179/vfsstream):gzopen(): cannot represent a stream of type user-space as a File Descriptor.

还尝试了以下方法:

    $in = fopen($source, 'rb');
    $out = fopen($destination, 'wb');
    stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, -1);
    while (!feof($in)) {
        fwrite($out, fread($in, 4096));
    }

但是生成的输出似乎不是有效的 GZIP(可能缺少 header?)

最后我尝试了这个:

    $in = fopen($source, 'rb');
    $out = fopen('compress.zlib://' . $destination, 'wb');
    while (!feof($in)) {
        fwrite($out, fread($in, 4096));
    }

但是(不出所料)如果 $destination 已经有一个包装器(例如 php://stdin 或上面提到的 vfs://),这将失败。

一定有办法做到这一点,但搜索没有找到任何例子。

我现在已经重新实现了 GZip header 和页脚的规范,这是将 stream_filter_append()zlib.deflate 一起使用时唯一缺少的东西(上面的第二个解决方案)。

(最小)header 由 https://www.rfc-editor.org/rfc/rfc1952#page-6 定义的十个字节组成:

1F 8B       // gzip format
08          // deflate compression
00          // flags
00 00 00 00 // four bytes for the file's mtime, zero if inapplicable or after 2038
00          // more flags
03          // operating system (03 for linux)

页脚由八个字节组成:四个字节用于未压缩有效负载的 CRC32 校验和,四个字节用于有效负载的字节长度(模 2^32)。

CRC32 在这里提出了另一个问题,因为 PHP 没有提供一种在不将整个有效负载加载到内存的情况下计算它的方法,而我们正在努力避免这种情况。

我重新实现了 Mark Adler 的 crc32_combine 算法,使用两个字符串的 CRC32 校验和(以及第二个字符串的长度)来计算它们串联的 CRC32 校验和:https://github.com/madler/zlib/blob/v1.2.11/crc32.c#L372 这允许在加载和压缩每个块时更新 CRC32。