65.9K
CodeProject 正在变化。 阅读更多。
Home

在 PHP 中流式传输 ZIP 文件而不使用临时文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (3投票s)

2019年6月7日

CPOL

10分钟阅读

viewsIcon

28511

downloadIcon

472

在不使用临时文件的情况下,即时流式传输 ZIP 文件中的多个文件或目录。

引言

在我尝试将“下载目录”功能添加到我的自定义 Web 应用程序时,我找到的所有解决方案都基于先创建 ZIP 文件然后发送它。在我的情况下,这可能导致生成大型临时文件(由于它们大多是图像),这些文件本身也无法压缩。

因此,我提出了一个想法,即直接在原始数据周围即时创建一个未压缩的 ZIP 存档——正如我发现的那样,这非常简单。

背景

为此,只需考虑 ZIP 存档的最低必要结构就足够了:我们不需要多部分文件,也不需要为每个文件存储额外的​​信息——当然,我们也不需要了解任何压缩算法。

ZIP 存档的基本结构使其可以轻松地即时组装

文件条目 1
  文件头 1
  文件数据 1
文件条目 2
  文件头 2
  文件数据 2
...
文件条目 n
  文件头 n
  文件数据 n
目录条目 1
目录条目 2
...
目录条目 n
目录结束

详细来说,让我们深入研究一些字节。这是一个 ZIP 文件,其中包含一个未压缩的文本文件“test.txt”,该文件包含文本“The quick brown fox jumps over the lazy dog.” 我为上面的每个区域和每个值着色,并包含每个值含义的摘要。

通用数据类型

  • UInt16 - 一个 2 字节、16 位数字,采用小端字节序(例如 0x1234 = [34, 12])
  • UInt32 - 一个 4 字节、32 位数字,采用小端字节序(例如 0x12345678 = [78, 56, 34, 12])
  • DateTime - 一个精确到秒的时间戳,位格式为 YYYYYYYmmmmddddd HHHHHiiiiiisssss,采用小端字节序,例如 2019-01-23 22:33:44
      二进制 就地
    年份 -1980 39 0b00100111 0b01001110001101111011010000110110
    1 0b00000001 0b01001110001101111011010000110110
    23 0b00010111 0b01001110001101111011010000110110
    小时 22 0b00010110 0b01001110001101111011010000110110
    分钟 33 0b00100001 0b01001110001101111011010000110110
    秒/2 22 0b00010110 0b01001110001101111011010000110110

    0b01001110001101111011010000110110 = 0x4E37B436 => [36, B4, 37, 4E]

  • CRC-32 - 文件数据的 4 字节 CRC-32 校验和,使用魔术数字 0xdebb20e3。在 PHP 中,这是名为“crc32b”的哈希算法。

文件条目

文件条目是描述文件并包含其数据的一部分。文件条目堆叠在一起。

名称 长度 数据类型 描述
签名 4 签名 文件条目签名,由“PK”后跟字节 03 和 04 组成
版本 2 UInt16 存档的宿主系统和兼容版本 - 在此,我仅使用 0x000A 表示 Windows/NTFS,但它实际上无关紧要
标志 2 UInt16 读取此文件的选项 - 在此,我使用 0x0800,表示 UTF-8 编码的文件名和注释,除此之外无其他
压缩方法 2 UInt16 数据压缩方法 - 在此,使用 0x0000,表示“未压缩”
文件时间 4 UInt32 文件的最后修改时间,不保存其他时间,格式见上
校验和 4 UInt32 文件数据的 CRC-32 校验和,格式见上
压缩大小 4 UInt32 压缩文件数据的大小 - 在此,与文件大小相同
未压缩大小 4 UInt32 未压缩文件的大小
文件名长度 2 UInt16 文件名的长度
附加数据长度 2 UInt16 附加数据的长度 - 在此,不使用附加数据,因此始终为 0x0000
文件名 * 字符串 UTF-8 编码的文件名
文件数据 * 字节 文件数据 - 通常是压缩的,但在本例中只是原始数据
附加数据 * 特殊 附加数据,例如创建时间、属性等 - 在此,未使用

中央目录条目

中央目录条目包含有关文件条目的更详细数据。中央目录条目堆叠在一起,构成一种目录。

名称 长度 数据类型 描述
签名 4 签名 中央目录条目签名,由“PK”后跟字节 01 和 02 组成
操作系统版本 2 UInt16 创建存档的版本 - 在此,我仅使用 0x003F
版本 2 UInt16 解压所需的最低版本 - 在此,我仅使用 0x000A
标志 2 UInt16 读取此文件的选项 - 在此,我使用 0x0800,表示 UTF-8 编码的文件名和注释,除此之外无其他
压缩方法 2 UInt16 数据压缩方法 - 在此,使用 0x0000,表示“未压缩”
文件时间 4 日期时间 文件的最后修改时间,不保存其他时间,格式见上
校验和 4 CRC32 文件数据的 CRC-32 校验和,格式见上
压缩大小 4 UInt32 压缩文件数据的大小 - 在此,与文件大小相同
未压缩大小 4 UInt32 未压缩文件的大小
文件名长度 2 UInt16 文件名的长度
附加数据长度 2 UInt16 附加数据的长度 - 在此,不使用附加数据,因此始终为 0x0000
注释长度 2 UInt16 文件注释的长度
磁盘 2 UInt16 文件所在磁盘编号 - 在此,我仅使用一个文件,因此始终为 0x0000
内部属性 2 UInt16 内部使用的属性 - 在此,未使用,始终为 0x0000
外部属性 4 UInt32 外部使用的属性 - 在此,未使用,始终为 0x00000000
文件条目偏移量 4 UInt32 此中央目录条目开始的文件条目在文件内的偏移量
文件名 * 字符串 UTF-8 编码的文件名
附加数据 * 特殊 附加数据,例如创建时间、属性等 - 在此,未使用
注释 * 字符串 描述文件的注释

中央目录条目结束

此条目仅出现一次 - 至少在此场景下 - 直接堆叠在最后一个中央目录条目之上。

名称 长度 数据类型 描述
签名 4 签名 中央目录条目签名,由“PK”后跟字节 05 和 06 组成
磁盘索引 2 UInt16 此磁盘的索引 - 在此,我不使用多个磁盘,因此始终为 0x0000
起始磁盘 2 UInt16 中央目录开始的磁盘索引 - 在此,我不使用多个磁盘,因此始终为 0x0000
文件数,磁盘 2 UInt16 此磁盘上的文件数 - 在此,始终为包含文件的总数
文件数,中央目录 2 UInt16 此中央目录中的文件数 - 在此,始终为包含文件的总数
大小 4 UInt32 中央目录的大小,不包括此条目
偏移量 4 UInt32 此磁盘上第一个中央目录条目的偏移量 - 在此,始终为该文件第一个中央目录条目的偏移量
注释长度 2 UInt16 存档注释的长度
注释 * 字符串 存档注释

Using the Code

该代码是一个名为 BjSZipper 的 PHP 类,它根据您选择使用的功能,包含静态和实例功能。在这两种情况下,都只有文件信息存储在内存中,文件数据是即时流式传输的。

1. 收集信息然后发送(实例)

此方法使用类的实例,收集要发送的每个文件的信息(包括计算 CRC-32 校验和),然后开始发送存档。对用户的好处是他们可以看到进度条,因为客户端可以提前知道存档大小。缺点是请求后下载开始的时间稍晚——尤其是在处理大量文件或大型文件时。

方法

__construct($zipName = "download.zip", $comment = "")

BjSZipper 的构造函数。接受两个参数

  • $zipName - 发送给客户端的 ZIP 存档的文件名,可选,默认为“download.zip
  • $comment - 存档注释,可选,默认为空
AddDir($path, $recursive = true, $filter = null)

准备一个路径及其内容以包含在 zip 存档中。路径相对于 $path 到存档根目录。接受三个参数

  • $path - 要从中获取文件的目录路径
  • $recursive - 一个 bool,如果为 true,则递归扫描目录,可选,默认为 true
  • $filter - 用于包含文件的正则表达式,可选,默认包含所有文件
AddFile($file, $name = null, $relativePath = "", $comment = "")

准备将单个文件包含在存档中。接受四个参数

  • $file - 文件的完整路径
  • $name - 存档中文件的名称,可选,默认为文件的基本名称
  • $relativePath - 存档内的文件路径,可选,默认为存档根目录,使用斜杠 '/' 作为路径分隔符
  • $comment - 文件注释,可选,默认为空
AddData($data, $name, $relativePath = '', $comment = '', $filetime = null)

准备从原始数据发送单个文件。接受五个参数

  • $data - 文件的原始数据,存储在内存中
  • $name - 存档中文件的名称
  • $relativePath - 存档内的文件路径,可选,默认为存档根目录,使用斜杠 '/' 作为路径分隔符
  • $comment - 文件注释,可选,默认为空
  • $filetime - 文件的最后修改时间,可选,默认为当前时间
Clear()

重置实例以重新开始。

Send()

将收集到的文件以组装好的 ZIP 存档发送给客户端。

示例

require_once('BjSZipper.php');

// Create a new instance
$zip = new BjSZipper('images.zip');

// Add files and data to send
$zip->AddDir(dirname(__FILE__), true, '/\.(jpg|jpeg)/i'); // All JPEGs recursively
$zip->AddFile('/var/www/html/testdata.bin');              // Just a normal file
$zip->AddData('All the JPEG images.', 'desc.txt');        // A raw text file

// Start sending the archive
$zip->Send();

2. 立即开始发送(静态)

此方法采用静态方法。每个文件在收集其数据后都会直接发送,文件信息存储在内存中用于最终的中央目录。好处是客户端反应更快,因为下载在处理完第一个文件后立即开始,并且内存使用也略好,因为只存储了与存档相关的数据,并且在添加原始数据时不必保留以供以后发送。缺点是脚本无法知道最终的存档大小,因此客户端将没有进度显示。

方法

static Begin($zipName = 'downlaod.zip', $unlimitedTime = true)

将下载标头发送给客户端。接受两个参数

  • $zipName - 显示给客户端的存档文件名,可选,默认为 'download.zip'
  • $unlimitedTime - 如果为 true,则使用 set_time_limit(0) 禁用 PHP 执行时间限制,可选,默认为 true
static SendFile($file, $name = null, $relativePath = '', $comment = '')

将单个文件附加到存档流发送给客户端。接受四个参数

  • $file - 文件的完整路径
  • $name - 存档中文件的名称,可选,默认为文件的基本名称
  • $relativePath - 文件相对于存档根目录的路径,分隔符为斜杠 '/',可选,默认为存档根目录
  • $comment - 此文件的注释,可选,默认为空
static SendDir($path, $recursive, $filter = null)

将目录中的所有指定文件附加到存档流发送给客户端。所有文件都相对于 $path 添加到存档根目录。接受三个参数

  • $path - 要从中获取文件的目录的完整路径
  • $recursive - 如果为 true,则也搜索子目录,可选,默认为 true
  • $filter - 过滤要添加的文件的正则表达式,可选,默认为找到的所有文件
static SendData($data, $name, $relativePath = '', $comment = '', $filetime = null)

将原始数据的文件附加到存档流发送给客户端。接受五个参数

  • $data - 要附加的文件的原始数据
  • $name - 存档中文件的名称
  • $relativePath - 文件在存档中相对于存档根目录的路径,可选,默认为存档根目录
  • $comment - 此文件的注释,可选,默认为空
  • $filetime - 存档中文件的修改时间,可选,默认为当前时间
static End($comment = '')

将中央目录和结束部分发送给客户端,从而结束存档。接受一个参数

  • $comment - 存档的注释

示例

require_once('BjSZipper.php');

// Send the HTTP headers
BjSZipper::Begin('images.zip');

// Add files and data to send
BjSZipper::SendDir(dirname(__FILE__), true, '/\.(jpg|jpeg)/i'); // All JPEGs recursively
BjSZipper::SendFile('/var/www/html/testdata.bin');              // Just a normal file
BjSZipper::SendData('All the JPEG images.', 'desc.txt');        // A raw text file

// Send the archive directory and end the archive
BjSZipper::End();

关注点

我编写此代码的目的是使其能够工作——基本上没有包含任何安全措施,也没有任何异常处理。请在使用此代码时注意这一点。

历史

  • 版本 1.0:实例和静态功能
© . All rights reserved.