Continuação do artigo Como é o arquivo zip e o que podemos fazer com ele .
Prefácio
Bom dia
E novamente no ar, temos programação não convencional em PHP.
Em um artigo anterior, estimados leitores estavam interessados em compactação e streaming de zip. Hoje vamos tentar abrir um pouco esse tópico.
Vamos dar uma olhada
Qual é o problema dele? Bem, para ser justo, vale a pena notar que sua única vantagem é que ele funciona, e há problemas lá, mas ainda assim.
Na minha opinião, o principal problema é que devemos primeiro escrever o Local File Header (LFH) com crc32 e o tamanho do arquivo e, em seguida, o conteúdo do próprio arquivo.
O que isso ameaça? Ou carregamos o arquivo inteiro na memória, consideramos o crc32 para ele, escrevemos LFH e, em seguida, o conteúdo do arquivo é econômico do ponto de vista da E / S, mas é inaceitável em arquivos grandes. Ou então, lemos o arquivo duas vezes - primeiro para calcular o hash, e depois para ler o conteúdo e gravar no arquivo - economicamente do ponto de vista da RAM, mas, por exemplo, primeiro dobra a carga na unidade, o que não é necessariamente um SSD.
E se o arquivo estiver localizado remotamente e seu volume, por exemplo, 1,5 GB? Bem, você precisa carregar todos os 1,5 GB na memória ou esperar até que todos esses 1,5 GB sejam baixados e calcularemos o hash e depois baixá-los novamente para fornecer o conteúdo. No caso, se quisermos fornecer dinamicamente, por exemplo, um banco de dados de despejo, que, por exemplo, lemos no stdout, isso geralmente é inaceitável - os dados no banco de dados foram alterados, os dados de despejo serão alterados, haverá um hash completamente diferente e obteremos um arquivo inválido. Sim, as coisas estão ruins, é claro.
Estrutura do descritor de dados para registros de arquivamento de fluxo contínuo
Mas não desanime, a especificação ZIP permite escrever dados primeiro e depois colar a estrutura do Descritor de Dados (DD) após os dados, nos quais já haverá crc32, o comprimento dos dados compactados e o comprimento dos dados sem compactação. Para fazer isso, precisamos apenas 3 vezes por dia com o estômago vazio no LFH, especificar generalPurposeBitFlag igual a 0x0008 e crc32 , compressedSize e uncompressedSize especificar 0 . Depois dos dados, escrevemos a estrutura DD , que será algo como isto:
pack('LLLL', ...array_values([ 'signature' => 0x08074b50,
E no CDFH (Central Directory File Header) apenas o generalPurposeBitFlag é alterado , o restante dos dados deve ser real. Mas isso não é um problema, já que escrevemos CDFH após todos os dados, e hashes com comprimentos de dados são conhecidos em qualquer caso.
Isso é tudo, é claro, bom. Resta apenas implementar no PHP.
E a biblioteca Hash padrão nos ajudará bastante. Podemos criar um contexto de hash no qual será suficiente para encher pedaços com dados e, no final, obter o valor do hash. Obviamente, essa solução será um pouco mais complicada que o hash ('crc32b', $ content) , mas nos poupará apenas um monte inimaginável de recursos e tempo.
Parece algo como isto:
$hashCtx = hash_init('crc32b'); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $chunk = null; } $hash = hash_final($hashCtx);
Se tudo for feito corretamente, o valor não será diferente de
hash_file ('crc32b', $ source) ou
hash ('crc32b', file_get_content ($ source)) .
Vamos tentar, de alguma forma, agrupar tudo isso em uma única função, para que possamos ler o arquivo de uma maneira conveniente para nós e, no final, obter seu hash e comprimento. E os geradores nos ajudarão com isso:
function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; }
e agora podemos apenas
$reader = read('https://speed.hetzner.de/1GB.bin'); foreach ($reader as $chunk) {
Na minha opinião, é bastante simples e conveniente. Com um arquivo de 1 GB, meu consumo máximo de memória era de 2 MB.
Agora vamos tentar modificar o código do artigo anterior para que possamos usar esta função.
Roteiro final <?php function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10,
Na saída, devemos obter um arquivo Zip com o nome test.zip, no qual haverá um arquivo com o script acima e 100MB.bin, com aproximadamente 100 MB de tamanho.
Compactação em arquivos zip
Agora, temos praticamente tudo para compactar os dados e fazê-lo também em tempo real.
Assim como obtemos um hash atribuindo pequenos blocos às funções, também podemos compactar dados graças à maravilhosa biblioteca Zlib e suas funções deflate_init e deflate_add .
Parece algo como isto:
$deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); yield deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $chunk = null; }
Eu encontrei uma opção como esta que, em comparação com a anterior, adicionará alguns zeros no final.Título de spoiler while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH);
Mas descompacte o juramento, então tive que me livrar dessa simplificação.Vamos corrigir nosso leitor para compactar imediatamente nossos dados e, no final, retornar um hash, o comprimento dos dados sem compactação e o comprimento dos dados com compactação:
function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; }
e experimente um arquivo de 100 mb:
$reader = read('https://speed.hetzner.de/100MB.bin'); foreach ($reader as $chunk) {
O consumo de memória ainda mostra que não carregamos o arquivo inteiro na memória.
Vamos juntar tudo e finalmente obter um arquivador de scripts realmente real.
Diferentemente da versão anterior, nosso generalPurposeBitFlag mudará - agora seu valor é 0x0018 , assim como o compressionMethod - 8 (que significa Deflate ).
Roteiro final <?php function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10,
Como resultado, obtive um arquivo de 360183 bytes de tamanho (nosso arquivo de 100 MB foi compactado muito bem, no qual provavelmente apenas um conjunto de bytes idênticos), e o
descompactação mostrou que nenhum erro foi encontrado no arquivo.
Conclusão
Se eu tiver energia e tempo suficientes para outro artigo, tentarei mostrar como e, o mais importante, por que tudo isso pode ser usado.
Se você estiver interessado em mais alguma coisa sobre esse tópico - sugira nos comentários, tentarei responder à sua pergunta. Provavelmente não lidaremos com criptografia, porque o script já cresceu e, na vida real, esses arquivos, ao que me parece, não são usados com muita frequência.
Obrigado por sua atenção e por seus comentários.