Como é o arquivo zip e o que podemos fazer com ele. Parte 3 - Aplicação Prática

Continuação do artigo Como é o arquivo zip e o que podemos fazer com ele. Parte 2 - Descritor e compactação de dados .

Caros leitores, novamente, recebo você na transferência de Programação Não Convencional em PHP. Para entender o que está acontecendo, recomendo que você leia os dois artigos anteriores sobre arquivos zip: Como é um arquivo zip e o que podemos fazer com ele ? Como é um arquivo zip e o que podemos fazer com ele. Parte 2 - Descritor e compactação de dados

Anteriormente, falei sobre como criar arquivos usando apenas código PHP e não usando bibliotecas e extensões (incluindo o zip padrão), e também mencionei alguns cenários de uso. Hoje vou tentar dar um exemplo de um desses cenários.

Armazenaremos as imagens no arquivo morto em um servidor remoto e, se necessário, mostraremos uma imagem específica para o usuário sem fazer download ou descompactar o arquivo morto, mas apenas receber dados do próprio servidor, tirar foto especificamente e nada mais (bem, ninguém cancelou a sobrecarga dos cabeçalhos mas ainda).

Cozinhar


Um dos meus projetos de estimação tem um script simples que eu uso para baixar várias fotos e salvá-las no arquivo. O script aceita uma lista de links no json para inserir STDIN, fornece o próprio arquivo para STDOUT e json com uma estrutura de matriz em STDERR (é claro, isso é um pouco exagerado e não é necessário criar um arquivo usando PHP nativo, você pode usar ferramentas mais apropriadas para isso e, em seguida, apenas leia a estrutura - tentarei destacar esse ponto no futuro, mas, nesta fase, parece-me que será mais óbvio).

Este script, com algumas modificações, cito abaixo:

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]; } 

Vamos salvá-lo em algum lugar, chame zip.php e tente executá-lo.

Eu já tenho uma lista pronta de links, eu recomendo usá-lo, já que existem fotos de aproximadamente 18mb, e precisamos que o tamanho total do arquivo não exceda 20mb (um pouco mais tarde vou explicar o porquê) - https://gist.githubusercontent.com /userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json

Vamos correr assim:

 $ curl -s \ https://gist.githubusercontent.com/userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json \ | php zip.php \ 2> structure.json \ 1> photos.zip 

Quando o script termina seu trabalho, na saída devemos obter dois arquivos - photos.zip , recomendo verificar com o comando:

 $ unzip -tq photos.zip 

e structure.json , no qual armazenamos json com base64 de tudo o que está em nosso arquivo, exceto os dados em si - todas as estruturas LFH e CDFH, além do EOCD. Ainda não vejo nenhuma aplicação prática do EOCD separadamente do arquivo, mas a posição do deslocamento do LFH em relação ao início do arquivo e o comprimento dos dados estão indicados no CDFH. Portanto, sabendo o tamanho do LFH, certamente podemos saber em que posição os dados começarão e onde terminarão.

Agora precisamos fazer upload de nosso arquivo para algum servidor remoto.

Por exemplo, usarei o bot de telegrama - este é o mais fácil, e é por isso que era tão importante caber no limite de 20mb.

Registre o bot no @BotFather, se você ainda não tiver um, escreva algo de bem-vindo ao seu bot, procure sua mensagem em https://api.telegram.org/bot{{TOKEN►►/getUpdates , de onde isolamos a propriedade message.from .id = esse é o ID do seu bot de bate-papo.

Preencha nosso arquivo com você, obtido na etapa anterior:

 $ curl -F document=@"photos.zip" "https://api.telegram.org/bot{{TOKEN}}/sendDocument?chat_id={{CHAT ID}}" > stored.json 

Agora já temos dois arquivos json - structure.json e armazenados.json .

E se tudo correu bem, o arquivo stored.json conterá json com os campos ok iguais a true e também result.document.file_id, o que precisamos.

Protótipo


Agora que tudo está finalmente pronto, vamos ao que interessa:

 <?php define('TOKEN', ''); //    $structure = json_decode(file_get_contents('structure.json'), true, 512, JSON_THROW_ON_ERROR); $stored = json_decode(file_get_contents('stored.json'), true, 512, JSON_THROW_ON_ERROR); $selectedFile = $structure['files'][array_rand($structure['files'])]; $LFH = base64_decode($selectedFile['LFH']); $CDFH = base64_decode($selectedFile['CDFH']); $fileLength = unpack('Llen', substr($CDFH, 24, 4))['len']; $fileStart = unpack('Lpos', substr($CDFH, 42, 4))['pos'] + strlen($LFH); $fileEnd = $fileStart + $fileLength; $response = json_decode(file_get_contents('https://api.telegram.org/bot' . TOKEN . '/getFile?' . http_build_query([ 'file_id' => $stored['result']['document']['file_id'] ])), true, 512, JSON_THROW_ON_ERROR); header('Content-Type: image/jpeg'); readfile('https://api.telegram.org/file/bot' . TOKEN . '/' . $response['result']['file_path'], false, stream_context_create([ 'http' => ['header' => "Range: bytes={$fileStart}-{$fileEnd}"] ])); 

Neste script, selecionamos um arquivo aleatório da lista que temos no arquivo, localizamos a posição do início do LFH, adicionamos o comprimento do LFH a ele e, assim, obtemos a posição do início dos dados de um arquivo específico no arquivo.

Adicionando o tamanho do arquivo a isso, obtemos a posição do final dos dados.

Resta obter o endereço do arquivo em si (em um caso específico de telegrama) e basta fazer uma solicitação com o cabeçalho Range: bytes=<->-<->

(supondo que o servidor suporte solicitações de intervalo)

Como resultado, ao acessar o script especificado, veremos o conteúdo de uma imagem aleatória do nosso arquivo.

Não, bem, não é sério ...


Escusado será dizer que a solicitação para getFile deve ser armazenada em cache, porque os telegramas garantem que o link permanecerá por pelo menos uma hora. Em geral, este exemplo não serve para nada, apenas para mostrar que funciona.

Vamos nos esforçar um pouco mais - execute tudo no amphp. Afinal, as pessoas estão distribuindo estática na produção no node.js, mas o que nos impede (bem, além do senso comum, é claro)? Também temos assincronia aqui, você sabe.

Vamos precisar de 3 pacotes:

  • amphp / http-server - obviamente, o servidor para o qual iremos distribuir os arquivos
  • amphp / artax - cliente http com o qual extrairemos os dados, além de atualizar um link direto para o arquivo no cache.
  • amphp / parallel é uma biblioteca para multithreading e tudo mais. Mas não tenha medo, precisamos apenas do SharedMemoryParcel, o que nos servirá como um cache de im-memória.

Colocamos as dependências necessárias:

 $ composer require amphp/http-server amphp/artax amphp/parallel 

E escreva isso

Script
 <?php define('TOKEN', ''); //    use Amp\Artax\DefaultClient; use Amp\Artax\Request as ArtaxRequest; use Amp\Http\Server\RequestHandler\CallableRequestHandler; use Amp\Http\Server\Server as HttpServer; use Amp\Http\Server\Request; use Amp\Http\Server\Response; use Amp\Http\Status; use Amp\Parallel\Sync\SharedMemoryParcel; use Amp\Socket; use Psr\Log\NullLogger; require 'vendor/autoload.php'; Amp\Loop::run(function () { $structure = json_decode(file_get_contents('structure.json'), true, 512, JSON_THROW_ON_ERROR); $stored = json_decode(file_get_contents('stored.json'), true, 512, JSON_THROW_ON_ERROR); $parcel = SharedMemoryParcel::create($id = bin2hex(random_bytes(10)), []); $client = new DefaultClient(); $handler = function (Request $request) use ($parcel, $client, $structure, $stored) { $cached = yield $parcel->synchronized(function (array $value) use ($client, $stored) { if (!isset($value['file_path']) || $value['expires'] <= time()) { $response = yield $client->request('https://api.telegram.org/bot' . TOKEN . '/getFile?' . http_build_query([ 'file_id' => $stored['result']['document']['file_id'] ])); $json = json_decode(yield $response->getBody(), true, 512, JSON_THROW_ON_ERROR); $value = ['file_path' => $json['result']['file_path'], 'expires' => time() + 55 * 60]; } return $value; }); $selectedFile = $structure['files'][array_rand($structure['files'])]; $LFH = base64_decode($selectedFile['LFH']); $CDFH = base64_decode($selectedFile['CDFH']); $fileLength = unpack('Llen', substr($CDFH, 24, 4))['len']; $fileStart = unpack('Lpos', substr($CDFH, 42, 4))['pos'] + strlen($LFH); $fileEnd = $fileStart + $fileLength; $request = (new ArtaxRequest('https://api.telegram.org/file/bot' . TOKEN . '/' . $cached['file_path'])) ->withHeader('Range', "bytes={$fileStart}-{$fileEnd}"); $response = yield $client->request($request); if (!in_array($response->getStatus(), [200, 206])) { return new Response(Status::SERVICE_UNAVAILABLE, ['content-type' => 'text/plain'], "Service Unavailable."); } return new Response(Status::OK, ['content-type' => 'image/jpeg'], $response->getBody()); }; $server = new HttpServer([Socket\listen("0.0.0.0:10051")], new CallableRequestHandler($handler), new NullLogger); yield $server->start(); Amp\Loop::onSignal(SIGINT, function (string $watcherId) use ($server) { Amp\Loop::cancel($watcherId); yield $server->stop(); }); }); 

O que obtemos como resultado: agora temos um cache no qual salvamos um link em um documento por 55 minutos, o que também garante que não solicitaremos um link várias vezes seguidas se recebermos muitas solicitações no momento em que o cache expirar. Bem, tudo isso é claramente mais fácil do que o readfile com PHP-FPM (ou, se Deus não permitir, PHP-CGI).

Além disso, você pode aumentar o pool de instâncias amphp - SharedMemoryParcel, pelo nome, sugere que nosso cache atrapalhará os processos / threads. Bem, ou, se você ainda tiver preocupações saudáveis ​​sobre a confiabilidade desse design, proxy_pass e nginx .

Em geral, a idéia está longe de ser nova e, mesmo nos anos mais difíceis, o DownloadMaster permitiu que você baixasse um arquivo específico do arquivo Zip sem fazer o download do arquivo inteiro; portanto, não é necessário armazenar a estrutura separadamente, mas salva algumas solicitações. E se, por exemplo, queremos proxy do arquivo para o usuário em tempo real, isso afeta praticamente a velocidade da solicitação.

Por que isso pode ser útil? Para salvar o inode, por exemplo, quando você tem dezenas de milhões de arquivos pequenos e não deseja armazená-los diretamente no banco de dados, pode armazená-los em arquivos, sabendo o deslocamento para receber um único arquivo.

Ou se você estiver armazenando arquivos remotamente. Em um telegrama de 20mb, é claro, você não pode se locomover, mas quem sabe, existem outras opções.

Source: https://habr.com/ru/post/pt484520/


All Articles