Continuación del artículo Cómo se ve el archivo zip y qué podemos hacer con él .
Prólogo
Buen dia
Y nuevamente en el aire tenemos programación no convencional en PHP.
En un artículo anterior, los lectores estimados estaban interesados en la compresión zip y la transmisión zip. Hoy intentaremos abrir un poco este tema.
Echemos un vistazo a
Código del último artículo Cual es su problema? Bueno, para ser justos, vale la pena señalar que su única ventaja es que funciona, y hay problemas allí, pero aún así.
En mi opinión, el problema principal es que primero debemos escribir el Encabezado de archivo local (LFH) con crc32 y la longitud del archivo, y luego el contenido del archivo en sí.
¿Qué amenaza esto? O cargamos todo el archivo en la memoria, consideramos crc32 para ello, escribimos LFH , y luego el contenido del archivo es económico desde el punto de vista de E / S, pero es inaceptable con archivos grandes. O leemos el archivo 2 veces, primero para calcular el hash, y luego para leer el contenido y escribir en el archivo, económicamente desde el punto de vista de la RAM, pero, por ejemplo, primero duplica la carga en el disco, que no es necesariamente un SSD.
¿Y si el archivo se encuentra de forma remota y su volumen, por ejemplo, 1,5 GB? Bueno, debe cargar todos los 1.5GB en la memoria, o esperar hasta que se descarguen todos estos 1.5GB y calcularemos el hash, y luego los volveremos a descargar para dar el contenido. Si queremos dar sobre la marcha, por ejemplo, una base de datos de volcado, que, por ejemplo, leemos de stdout, esto generalmente es inaceptable: los datos en la base de datos han cambiado, los datos de volcado cambiarán, habrá un hash completamente diferente y obtendremos un archivo no válido. Sí, las cosas están mal, por supuesto.
Estructura del descriptor de datos para la transmisión de registros de archivo
Pero no se desanime, la especificación ZIP nos permite escribir datos primero y luego pegar la estructura del Descriptor de datos (DD) después de los datos, que ya contiene crc32, la longitud de los datos empaquetados y la longitud de los datos sin compresión. Para hacer esto, solo necesitamos 3 veces al día con el estómago vacío en LFH especifique generalPurposeBitFlag igual a 0x0008 , y crc32 , compressedSize y descompressedSize especifique 0 . Luego, después de los datos, escribimos la estructura DD , que se verá así:
pack('LLLL', ...array_values([ 'signature' => 0x08074b50,
Y en el encabezado del archivo del directorio central (CDFH) solo cambia el generalPurposeBitFlag , el resto de los datos deben ser reales. Pero esto no es un problema, ya que escribimos CDFH después de todos los datos, y en cualquier caso se conocen hashes con longitudes de datos.
Esto es todo, por supuesto, bueno. Solo queda implementar en PHP.
Y la biblioteca estándar de Hash nos ayudará mucho. Podemos crear un contexto hash en el que será suficiente para rellenar fragmentos con datos y, al final, obtener el valor hash. Por supuesto, esta solución será algo más engorrosa que el hash ('crc32b', $ content) , pero nos ahorrará una cantidad inimaginable de recursos y tiempo.
Se parece a esto:
$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);
Si todo se hace correctamente, el valor no será diferente de
hash_file ('crc32b', $ source) o
hash ('crc32b', file_get_content ($ source)) .
Intentemos de alguna manera resumir todo esto en una función, para que podamos leer el archivo de una manera conveniente para nosotros, y al final obtener su hash y longitud. Y los generadores nos ayudarán con esto:
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))]; }
y ahora solo podemos
$reader = read('https://speed.hetzner.de/1GB.bin'); foreach ($reader as $chunk) {
En mi opinión, es bastante simple y conveniente. Con un archivo de 1GB, mi consumo máximo de memoria fue de 2MB.
Ahora intentemos modificar el código del artículo anterior para que podamos usar esta función.
Guión 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,
En la salida, deberíamos obtener un archivo Zip con el nombre test.zip, en el que habrá un archivo con el script anterior y 100MB.bin, de aproximadamente 100 MB de tamaño.
Compresión en archivos zip
Ahora tenemos prácticamente todo para comprimir los datos y hacerlo también sobre la marcha.
Del mismo modo que obtenemos un hash al dar pequeños fragmentos a las funciones, también podemos comprimir datos gracias a la maravillosa biblioteca Zlib y sus funciones deflate_init y deflate_add .
Se parece a esto:
$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; }
Conocí una opción como esta que, en comparación con la anterior, agregará algunos ceros al final.Encabezado de spoiler while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH);
Pero descomprimir juró, así que tuve que deshacerme de esa simplificación.Arreglemos nuestro lector para que comprima inmediatamente nuestros datos, y al final nos devuelve un hash, la longitud de los datos sin compresión y la longitud de los datos con compresión:
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)) ]; }
y prueba un archivo de 100 mb:
$reader = read('https://speed.hetzner.de/100MB.bin'); foreach ($reader as $chunk) {
El consumo de memoria todavía muestra que no cargamos todo el archivo en la memoria.
Pongámoslo todo junto y finalmente obtengamos un archivo de script realmente real.
A diferencia de la versión anterior, nuestro generalPurposeBitFlag cambiará, ahora su valor es 0x0018 , así como el modo de compresión - 8 (lo que significa Deflate ).
Guión 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, obtuve un archivo de 360183 bytes de tamaño (nuestro archivo de 100 MB se comprimió muy bien, lo que probablemente sea solo un conjunto de bytes idénticos), y
descomprimir mostró que no se encontraron errores en el archivo.
Conclusión
Si tengo suficiente energía y tiempo para otro artículo, intentaré mostrar cómo y, lo más importante, por qué se puede usar todo esto.
Si está interesado en algo más sobre este tema, sugiéralo en los comentarios, intentaré responder a su pregunta. Lo más probable es que no tratemos con el cifrado, porque el script ya ha crecido, y en la vida real, según me parece, estos archivos no se usan con mucha frecuencia.
Gracias por su atención y por sus comentarios.