文章的继续zip存档的外观以及我们可以使用它做些什么。 第2部分-数据描述符和压缩 。亲爱的读者,我再次欢迎您使用PHP进行非常规编程的转移。 为了了解正在发生的事情,我建议您阅读前两篇有关zip归档
文件的文章:
zip归档文件是什么样子,我们可以做什么 ?
zip归档文件 是什么样子,我们可以使用它做什么。 第2部分-数据描述符和压缩之前,我谈到了如何仅使用PHP代码而不使用任何库和扩展名(包括标准
zip )来创建档案,还提到了一些使用场景。 今天,我将尝试给出其中一种情况的示例。
我们将图片存储在远程服务器上的存档中,并且如有必要,向用户显示特定的图片,而无需下载或解压缩存档,而仅从服务器本身接收数据,即专门拍摄的图片,仅此而已(嗯,没有人取消了标头的开销但仍然)。
做饭
我的一个宠物项目有一个简单的脚本,我可以用它下载一堆图片并将其保存到档案中。 该脚本接受json中的链接列表以输入STDIN,将存档本身提供给STDOUT,并在STDERR中将其具有数组结构的json(当然,这有点过头了,不必使用本机PHP创建存档,您可以为此使用更合适的工具,然后阅读结构-将来我会尝试强调这一点,但在现阶段,在我看来,这将更加明显)。
该脚本经过一些修改,下面引用:
zip.php<?php $buffer = ''; while (!feof(STDIN)) { $buffer .= fread(STDIN, 4096); } $photos = json_decode($buffer, true, 512, JSON_THROW_ON_ERROR); $skeleton = ['files' => []]; $written = 0; [$written, $skeleton] = writeZip($written, $skeleton, $photos); $CDFH_Offset = $written; foreach ($skeleton['files'] as $index => $info) { $written += fwrite(STDOUT, $info['CDFH']); $skeleton['files'][$index]['CDFH'] = base64_encode($info['CDFH']); } $skeleton['EOCD'] = pack('LSSSSLLS', 0x06054b50, 0, 0, $records = count($skeleton['files']), $records, $written - $CDFH_Offset, $CDFH_Offset, 0); $written += fwrite(STDOUT, $skeleton['EOCD']); $skeleton['EOCD'] = base64_encode($skeleton['EOCD']); fwrite(STDERR, json_encode($skeleton)); function writeZip($written, $skeleton, $files) { $c = curl_init(); curl_setopt_array($c, [ CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 50, CURLOPT_FOLLOWLOCATION => true, ]); foreach ($files as $index => $url) { $fileName = $index . '.jpg'; for ($i = 0; $i < 1; $i++ ) { try { curl_setopt($c, CURLOPT_URL, $url); [$content, $code, $contentLength] = [ curl_exec($c), (int) curl_getinfo($c, CURLINFO_HTTP_CODE), (int) curl_getinfo($c, CURLINFO_CONTENT_LENGTH_DOWNLOAD) ]; if ($code !== 200) { throw new \Exception("[{$index}] " . 'Photo download error (' . $code . '): ' . curl_error($c)); } if (strlen($content) !== $contentLength) { var_dump(strlen($content), $contentLength); throw new \Exception("[{$index}] " . 'Different content-length'); } if ((false === $imageSize = @getimagesizefromstring($content)) || $imageSize[0] < 1 || $imageSize[1] < 1) { throw new \Exception("[{$index}] " . 'Broken image'); } [$width, $height] = $imageSize; $t = null; break; } catch (\Throwable $t) {} } if ($t !== null) { throw new \Exception('Error: ' . $index . ' > ' . $url, 0, $t); } $fileInfo = [ 'versionToExtract' => 10, 'generalPurposeBitFlag' => 0, 'compressionMethod' => 0, 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => hexdec(hash('crc32b', $content)), 'compressedSize' => $size = strlen($content), 'uncompressedSize' => $size, 'filenameLength' => strlen($fileName), 'extraFieldLength' => 0, ]; $LFH_Offset = $written; $skeleton['files'][$index] = [ 'LFH' => pack('LSSSSSLLLSSa*', 0x04034b50, ...array_values($fileInfo + ['filename' => $fileName])), 'CDFH' => pack('LSSSSSSLLLSSSSSLLa*', 0x02014b50, 798, ...array_values($fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $LFH_Offset, 'filename' => $fileName, ])), 'width' => $width, 'height' => $height, ]; $written += fwrite(STDOUT, $skeleton['files'][$index]['LFH']); $written += fwrite(STDOUT, $content); $skeleton['files'][$index]['LFH'] = base64_encode($skeleton['files'][$index]['LFH']); } curl_close($c); return [$written, $skeleton]; }
让我们将其保存在某个地方,调用zip.php并尝试运行它。
我已经有一个现成的链接列表,我建议使用它,因为有大约18mb的图片,并且我们需要的总档案大小不超过20mb(稍后我会告诉原因)-https://gist.githubusercontent.com
/userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json我们将像这样运行:
$ curl -s \ https://gist.githubusercontent.com/userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json \ | php zip.php \ 2> structure.json \ 1> photos.zip
脚本完成工作后,在输出中我们应该获得两个文件
-photos.zip ,我建议使用以下命令进行检查:
$ unzip -tq photos.zip
和
structure.json ,其中我们将json和我们存档中所有内容的base64存储在一起,除了数据本身-所有LFH和CDFH结构以及EOCD。 我还没有看到EOCD的任何实际应用与存档无关,但是LFH偏移相对于文件开头的位置以及数据长度在CDFH中显示。 因此,知道LFH的长度后,我们当然可以知道数据将从何处开始以及在何处结束。
现在我们需要将文件上传到某个远程服务器。
例如,我将使用电报bot-这是最简单的,这就是为什么适应20mb限制如此重要的原因。
使用@BotFather注册该机器人,如果您还没有机器人,请向您的机器人写一些欢迎的信息,在
https://api.telegram.org/bot{{TOKEN►►/getUpdates ,从中隔离message.from属性.id =这是您的聊天机器人的ID。
将您在上一步中获得的归档文件与您一起填写:
$ curl -F document=@"photos.zip" "https://api.telegram.org/bot{{TOKEN}}/sendDocument?chat_id={{CHAT ID}}" > stored.json
现在我们已经有了两个json文件
-structure.json和
stored.json 。
如果一切顺利,则stored.json文件将包含具有确定字段等于true的json,以及我们需要的result.document.file_id。
样机
现在一切都准备就绪,让我们开始做生意:
<?php define('TOKEN', '');
在此脚本中,我们从存档中的列表中选择一个随机文件,找到LFH起始位置,向其中添加LFH长度,从而获得存档中特定文件的数据的起始位置。
加上文件长度,我们得到数据结尾的位置。
剩下的只是获取文件本身的地址(在特殊情况下是用电报),然后足以简单地以“
Range: bytes=<->-<->
为标题发出请求
Range: bytes=<->-<->
(假设服务器支持范围请求)
结果,当访问指定的脚本时,我们将从存档中看到随机图像的内容。
不,这不严重...
毋庸置疑 ,对
getFile的请求应该被缓存,因为电报保证该链接至少存在一个小时。 总的来说,这个例子除了证明它是有效的之外,无济于事。
让我们再努力一点-在amphp上全部运行。 毕竟,人们在node.js上的生产中分发静态变量,但是是什么阻止了我们(当然,除了常识之外)? 您也知道,我们在这里也有异步性。
我们将需要3个软件包:
- amphp / http-server-显然,我们将文件分发到的服务器
- amphp / artax-我们将通过其获取数据的http客户端,以及更新指向缓存中文件的直接链接。
- amphp / parallel是一个用于多线程的库。 但是请不要害怕,我们只需要其中的SharedMemoryParcel,它将用作内存缓存。
我们放入必要的依赖项:
$ composer require amphp/http-server amphp/artax amphp/parallel
并写这个
剧本 <?php define('TOKEN', '');
结果是:我们现在有了一个缓存,在其中将指向文档的链接保存了55分钟,这还确保了,如果在缓存过期时收到很多请求,我们将不会连续多次请求链接。 好吧,所有这些显然比使用PHP-FPM(或者,天哪,PHP-CGI)的readfile容易。
此外,您可以提高amphp实例池-SharedMemoryParcel的名称,暗示我们的缓存将在进程/线程之间混乱。 好吧,或者,如果您仍然对此设计的可靠性有健康的担忧,那么可以使用
proxy_pass和nginx 。
总的来说,这个想法并不是什么新鲜事,甚至在大胡子的岁月里,DownloadMaster允许您从Zip存档中下载特定文件而无需下载整个存档,因此不必单独存储结构,但是可以节省一些请求。 例如,如果我们要动态将文件代理给用户该怎么办,它将在很大程度上影响请求的速度。
为什么这会派上用场? 例如,为了保存inode,当您有数以千万计的小文件并且不想直接将它们存储在数据库中时,可以将它们存储在归档中,知道偏移量以接收单个文件。
或者,如果您要远程存储文件。 当然,您无法绕开20mb的电报,但是谁知道,还有其他选择。