Cómo se ve el archivo zip y qué podemos hacer con él. Parte 3 - Aplicación práctica

Continuación del artículo Cómo se ve el archivo zip y qué podemos hacer con él. Parte 2 - Descriptor de datos y compresión .

Estimados lectores, nuevamente les doy la bienvenida a la transferencia de Programación no convencional en PHP. Para comprender lo que está sucediendo, le recomiendo que lea los dos artículos anteriores sobre archivos zip: ¿Cómo se ve un archivo zip y qué podemos hacer con él ? ¿Cómo se ve un archivo zip y qué podemos hacer con él? Parte 2 - Descriptor de datos y compresión

Anteriormente, hablé sobre cómo crear archivos usando solo código PHP y no usando ninguna biblioteca y extensión (incluido el zip estándar), y también mencioné algunos escenarios de uso. Hoy intentaré dar un ejemplo de uno de estos escenarios.

Almacenaremos las imágenes en el archivo en un servidor remoto y, si es necesario, mostraremos una imagen específica al usuario sin descargar o desempaquetar el archivo, sino solo recibir datos del propio servidor, imagen tomada específicamente y nada más (bueno, nadie canceló la sobrecarga de los encabezados pero aun así).

Cocinando


Uno de mis proyectos favoritos tiene un script simple que utilizo para descargar un montón de imágenes y guardarlas en el archivo. El script acepta una lista de enlaces en json para ingresar a STDIN, le da el archivo a STDOUT y json con una estructura de matriz en STDERR (por supuesto, esto es un poco excesivo y no es necesario crear un archivo usando PHP nativo, puede usar herramientas más apropiadas para esto, y luego simplemente lea la estructura: intentaré resaltar este punto en el futuro, pero en esta etapa, me parece, será más obvio).

Este script, con algunas modificaciones, cito a continuación:

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 a guardarlo en algún lugar, llame a zip.php e intente ejecutarlo.

Ya tengo una lista de enlaces ya preparada, recomiendo usarla, ya que hay imágenes de ~ 18mb, y necesitamos que el tamaño total del archivo no supere los 20mb (un poco más tarde explicaré por qué) - https://gist.githubusercontent.com /userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json

Vamos a correr así:

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

Cuando el script termina su trabajo, en la salida deberíamos obtener dos archivos: photos.zip , recomiendo verificarlo con el comando:

 $ unzip -tq photos.zip 

y structure.json , en el que almacenamos json con base64 de todo lo que está en nuestro archivo, excepto los datos en sí, todas las estructuras LFH y CDFH, así como EOCD. Todavía no veo ninguna aplicación práctica de EOCD por separado del archivo, pero la posición del desplazamiento de LFH en relación con el comienzo del archivo y la longitud de los datos se indican en CDFH. Entonces, conociendo la longitud de la LFH, ciertamente podemos saber desde qué posición comenzarán los datos y dónde terminarán.

Ahora necesitamos subir nuestro archivo a algún servidor remoto.

Por ejemplo, usaré el bot de telegramas: esta es la forma más fácil, por eso era tan importante encajar en el límite de 20 mb.

Registre el bot con @BotFather, si aún no tiene uno, escriba algo de bienvenida a su bot, busque su mensaje en https://api.telegram.org/bot{{TOKEN►►/getUpdates , desde donde aislamos la propiedad message.from .id = esta es la identificación de su bot de chat.

Rellene nuestro archivo con usted, obtenido en el paso anterior:

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

Ahora ya tenemos dos archivos json: structure.json y shown.json .

Y si todo salió bien, el archivo store.json contendrá json con los campos ok iguales a verdadero, y también result.document.file_id, que necesitamos.

Prototipo


Ahora que todo está finalmente listo, pasemos a los negocios:

 <?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}"] ])); 

En este script, seleccionamos un archivo aleatorio de la lista que tenemos en el archivo, encontramos la posición del comienzo de LFH, le agregamos la longitud de LFH y, por lo tanto, obtenemos la posición del comienzo de los datos de un archivo en particular en el archivo.

Agregando la longitud del archivo a esto, obtenemos la posición del final de los datos.

Queda por obtener la dirección del archivo en sí (esto es en un caso particular con un telegrama) y luego es suficiente simplemente hacer una solicitud con el encabezado Range: bytes=<->-<->

(suponiendo que el servidor admite solicitudes de rango)

Como resultado, al acceder al script especificado, veremos el contenido de una imagen aleatoria de nuestro archivo.

No, bueno, no es serio ...


No hace falta decir que la solicitud de getFile debe almacenarse en caché, porque los telegramas garantizan que el enlace durará al menos una hora. En general, este ejemplo no sirve para nada más que para demostrar que funciona.

Probemos un poco más duro: ejecútelo todo en amphp. Después de todo, las personas están distribuyendo estadísticas en producción en node.js, pero ¿qué nos impide (bueno, aparte del sentido común, por supuesto)? Tenemos asincronía aquí también, ya sabes.

Necesitaremos 3 paquetes:

  • amphp / http-server : obviamente, el servidor al que distribuiremos los archivos
  • amphp / artax : http-client con el que extraeremos los datos y actualizaremos un enlace directo al archivo en el caché.
  • amphp / parallel es una biblioteca para subprocesos múltiples y todo eso. Pero no tenga miedo, solo necesitamos SharedMemoryParcel, que nos servirá como caché de memoria.

Ponemos las dependencias necesarias:

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

Y escribe esto

Guión
 <?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(); }); }); 

Lo que obtenemos como resultado: ahora tenemos un caché donde guardamos un enlace a un documento durante 55 minutos, lo que también garantiza que no solicitaremos un enlace varias veces seguidas si recibimos muchas solicitudes en el momento en que caduca el caché. Bueno, todo esto es claramente más fácil que readfile con PHP-FPM (o, Dios no lo quiera, PHP-CGI).

Además, puede aumentar el grupo de instancias de amphp: SharedMemoryParcel, por su nombre, sugiere que nuestra memoria caché buscará entre procesos / subprocesos. Bueno, o, si aún tiene preocupaciones saludables sobre la confiabilidad de este diseño, entonces proxy_pass y nginx .

En general, la idea está lejos de ser nueva e incluso en los años barbudos DownloadMaster le permitió descargar un archivo específico del archivo Zip sin descargar todo el archivo, por lo tanto, no es necesario almacenar la estructura por separado, pero guarda un par de solicitudes. ¿Qué pasa si, por ejemplo, deseamos enviar el archivo al usuario sobre la marcha, esto afecta la velocidad de la solicitud?

¿Por qué esto puede ser útil? Para guardar el inodo, por ejemplo, cuando tiene decenas de millones de archivos pequeños y no desea almacenarlos directamente en la base de datos, puede almacenarlos en archivos, conociendo el desplazamiento para recibir un solo archivo.

O si está almacenando archivos de forma remota. En un telegrama, de 20mb, por supuesto, no puedes moverte, pero quién sabe, hay otras opciones.

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


All Articles