À quoi ressemble l'archive zip et ce que nous pouvons en faire. Partie 3 - Application pratique

Suite de l'article A quoi ressemble l'archive zip et ce que nous pouvons en faire. Partie 2 - Descripteur de données et compression .

Chers lecteurs, je vous souhaite à nouveau la bienvenue au transfert de la programmation non conventionnelle en PHP. Pour comprendre ce qui se passe, je vous recommande de lire les deux articles précédents sur les archives zip: À quoi ressemble une archive zip et que pouvons-nous en faire ? À quoi ressemble une archive zip et que pouvons-nous en faire. Partie 2 - Descripteur de données et compression

Plus tôt, j'ai expliqué comment créer des archives en utilisant uniquement du code PHP et sans utiliser de bibliothèques et d'extensions (y compris le zip standard), et j'ai également mentionné certains scénarios d'utilisation. Aujourd'hui, je vais essayer de donner un exemple de l'un de ces scénarios.

Nous allons stocker les images dans l'archive sur un serveur distant et, si nécessaire, montrer une image spécifique à l'utilisateur sans télécharger ou décompresser l'archive, mais uniquement recevoir des données du serveur lui-même, une photo spécifiquement prise et rien de plus (eh bien, personne n'a annulé la surcharge pour les en-têtes mais quand même).

Cuisine


Un de mes projets pour animaux de compagnie a un script simple que j'utilise pour télécharger un tas de photos et les enregistrer dans les archives. Le script accepte une liste de liens dans json pour entrer dans STDIN, donne l'archive elle-même à STDOUT et json avec une structure de tableau dans STDERR (bien sûr, c'est un peu exagéré et il n'est pas nécessaire de créer une archive en utilisant PHP natif, vous pouvez utiliser des outils plus appropriés pour cela, puis juste lire la structure - je vais essayer de mettre en évidence ce point à l'avenir, mais à ce stade, il me semble, ce sera plus évident).

Ce script, avec quelques modifications, je cite ci-dessous:

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

Enregistrons-le quelque part, appelons zip.php et essayons de l'exécuter.

J'ai déjà une liste de liens prête à l'emploi, je recommande de l'utiliser, car il y a des images d'environ ~ 18 Mo, et nous avons besoin que la taille totale de l'archive ne dépasse pas 20 Mo (un peu plus tard, je dirai pourquoi) - https://gist.githubusercontent.com /userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json

Nous allons courir comme ceci:

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

Lorsque le script a terminé son travail, à la sortie, nous devrions obtenir deux fichiers - photos.zip , je recommande de le vérifier avec la commande:

 $ unzip -tq photos.zip 

et structure.json , dans lequel nous stockons json avec base64 de tout ce qui est dans nos archives, à l'exception des données elles-mêmes - toutes les structures LFH et CDFH, ainsi que EOCD. Je ne vois pas encore d’application pratique d’EOCD séparément de l’archive, mais la position du décalage LFH par rapport au début du fichier et la longueur des données sont indiquées dans CDFH. Ainsi, connaissant la longueur de la LFH, nous pouvons certainement savoir à partir de quelle position les données commenceront et où elles se termineront.

Maintenant, nous devons télécharger notre fichier sur un serveur distant.

Par exemple, j'utiliserai le bot de télégramme - c'est le plus simple, c'est pourquoi il était si important de tenir dans la limite de 20 Mo.

Enregistrez le bot avec @BotFather, si vous n'en avez pas déjà un, écrivez quelque chose de bienvenu à votre bot, recherchez votre message dans https://api.telegram.org/bot{{TOTOK►►/getUpdates , d'où nous isolons la propriété message.from .id = c'est l'identifiant de votre bot de chat.

Remplissez nos archives avec vous, obtenues à l'étape précédente:

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

Maintenant, nous avons déjà deux fichiers json - structure.json et stored.json .

Et si tout s'est bien passé, le fichier stored.json contiendra json avec des champs ok égaux à true, ainsi que result.document.file_id, dont nous avons besoin.

Prototype


Maintenant que tout est enfin prêt, passons aux choses sérieuses:

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

Dans ce script, nous sélectionnons un fichier aléatoire dans la liste que nous avons dans l'archive, trouvons la position du début de LFH, y ajoutons la longueur de LFH et obtenons ainsi la position du début des données d'un fichier particulier dans l'archive.

En ajoutant la longueur du fichier à cela, nous obtenons la position de la fin des données.

Il reste à obtenir l'adresse du fichier lui-même (c'est dans un cas particulier avec un télégramme) puis à faire une requête avec l'en-tête Range: bytes=<->-<->

(en supposant que le serveur prend en charge les demandes de plage)

Par conséquent, lors de l'accès au script spécifié, nous verrons le contenu d'une image aléatoire de nos archives.

Non, eh bien, ce n'est pas grave ...


Il va sans dire que la demande de getFile doit être mise en cache, car les télégrammes garantissent que le lien durera au moins une heure. En général, cet exemple n'est bon à rien que pour montrer qu'il fonctionne.

Essayons un peu plus - exécutez le tout sur amphp. Après tout, les gens distribuent des statiques en production sur node.js, mais qu'est-ce qui nous empêche (enfin, à part le bon sens, bien sûr)? Nous avons aussi l'asynchronie, vous savez.

Nous aurons besoin de 3 packages:

  • amphp / http-server - évidemment, le serveur vers lequel nous distribuerons les fichiers
  • amphp / artax - client http avec lequel nous allons extraire les données, ainsi que mettre à jour un lien direct vers le fichier dans le cache.
  • amphp / parallel est une bibliothèque pour le multithreading et tout ça. Mais n'ayez pas peur, nous n'avons besoin que de SharedMemoryParcel, qui nous servira de cache im-memory.

Nous mettons les dépendances nécessaires:

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

Et écris ceci

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(); }); }); 

Résultat: nous avons maintenant un cache où nous enregistrons un lien vers un document pendant 55 minutes, ce qui garantit également que nous ne demanderons pas un lien plusieurs fois de suite si nous recevons beaucoup de demandes au moment où le cache expire. Eh bien, tout cela est clairement plus facile que le fichier de lecture avec PHP-FPM (ou, Dieu nous en préserve, PHP-CGI).

De plus, vous pouvez augmenter le pool d'instances amphp - SharedMemoryParcel, par son nom, indique que notre cache va tâtonner entre les processus / threads. Eh bien, ou, si vous avez encore de sérieuses préoccupations concernant la fiabilité de cette conception, alors proxy_pass et nginx .

En général, l'idée est loin d'être nouvelle et même dans les années barbus, DownloadMaster vous a permis de télécharger un fichier spécifique à partir de l'archive Zip sans télécharger l'archive entière, il n'est donc pas nécessaire de stocker la structure séparément, mais il enregistre quelques demandes. Et si, par exemple, nous voulons proxy le fichier à l'utilisateur à la volée, cela affecte à peu près la vitesse de la demande.

Pourquoi cela pourrait-il être utile? Pour enregistrer l'inode, par exemple, lorsque vous avez des dizaines de millions de petits fichiers et que vous ne souhaitez pas les stocker directement dans la base de données, vous pouvez les stocker dans des archives, connaissant le décalage pour recevoir un seul fichier.

Ou si vous stockez des fichiers à distance. Dans un télégramme de 20 Mo, bien sûr, vous ne pouvez pas vous déplacer, mais qui sait, il existe d'autres options.

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


All Articles