Wie das Zip-Archiv aussieht und was wir damit machen können. Teil 3 - Praktische Anwendung

Fortsetzung des Artikels Wie das Zip-Archiv aussieht und was wir damit machen können. Teil 2 - Datenbeschreibung und -komprimierung .

Sehr geehrte Leserinnen und Leser, ich begrüße Sie erneut zum Transfer von Unconventional Programming in PHP. Um zu verstehen, was passiert, empfehle ich Ihnen, die beiden vorherigen Artikel über Zip-Archive zu lesen: Wie sieht ein Zip-Archiv aus und was können wir damit machen ? Wie sieht ein Zip-Archiv aus und was können wir damit machen ? Teil 2 - Datenbeschreibung und -komprimierung

Zuvor habe ich darüber gesprochen, wie Archive nur mit PHP-Code und ohne Bibliotheken und Erweiterungen (einschließlich der Standard- Zip-Datei ) erstellt werden, und auch einige Verwendungsszenarien erwähnt. Heute werde ich versuchen, ein Beispiel für eines dieser Szenarien zu geben.

Wir speichern die Bilder im Archiv auf einem Remote-Server und zeigen dem Benutzer bei Bedarf ein bestimmtes Bild an, ohne das Archiv herunterzuladen oder zu entpacken, aber nur Daten vom Server selbst zu empfangen, speziell aufgenommenes Bild und nichts weiter (nun ja, niemand hat den Overhead für Header abgebrochen aber immer noch).

Kochen


Eines meiner Lieblingsprojekte verfügt über ein einfaches Skript, mit dem ich eine Reihe von Bildern herunterlade und im Archiv speichere. Das Skript akzeptiert eine Liste von Links in json, um STDIN einzugeben, gibt das Archiv selbst an STDOUT und json mit einer Array-Struktur in STDERR (dies ist natürlich ein bisschen übertrieben, und es ist nicht erforderlich, ein Archiv mit nativem PHP zu erstellen, Sie können dafür geeignetere Tools verwenden und dann einfach Lesen Sie die Struktur - Ich werde versuchen, diesen Punkt in der Zukunft hervorzuheben, aber zu diesem Zeitpunkt scheint es mir, dass es offensichtlicher sein wird).

Dieses Skript, mit einigen Änderungen, zitiere ich unten:

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

Speichern wir es irgendwo, rufen zip.php auf und versuchen es auszuführen.

Ich habe bereits eine fertige Liste mit Links, ich empfehle sie zu verwenden, da es Bilder über ~ 18 MB gibt und die Gesamtgröße des Archivs 20 MB nicht überschreiten darf (etwas später werde ich erklären, warum) - https://gist.githubusercontent.com /userqq/d0ca3aba6b6762c9ce5bc3ace92a9f9e/raw/70f446eb98f1ba6838ad3c19c3346cba371fd263/photos.json

Wir werden so laufen:

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

Wenn das Skript seine Arbeit beendet hat, sollten wir am Ausgang zwei Dateien erhalten - photos.zip . Ich empfehle, dies mit dem Befehl zu überprüfen:

 $ unzip -tq photos.zip 

und structure.json , in dem wir json mit base64 von allem speichern, was sich in unserem Archiv befindet, mit Ausnahme der Daten selbst - alle LFH- und CDFH-Strukturen sowie EOCD. Ich sehe noch keine praktische Anwendung von EOCD getrennt vom Archiv, aber die Position des LFH-Offsets relativ zum Dateianfang und die Datenlänge sind in CDFH angegeben. Wenn wir also die Länge des LFH kennen, können wir sicher wissen, von welcher Position aus die Daten beginnen und wo sie enden werden.

Jetzt müssen wir unsere Datei auf einen Remote-Server hochladen.

Zum Beispiel verwende ich den Telegramm-Bot - dies ist der einfachste, weshalb es so wichtig war, in das 20-MB-Limit zu passen.

Registrieren Sie den Bot bei @BotFather, wenn Sie noch keinen haben, schreiben Sie etwas Willkommenes in Ihren Bot, suchen Sie Ihre Nachricht unter https://api.telegram.org/bot{{TOKEN►►/getUpdates , von dem wir die Eigenschaft message.from isolieren .id = Dies ist die ID Ihres Chat-Bot.

Füllen Sie unser Archiv mit Ihnen, das Sie im vorherigen Schritt erhalten haben:

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

Jetzt haben wir bereits zwei json-Dateien - structure.json und saved.json .

Und wenn alles gut gegangen ist, enthält die Datei saved.json json mit ok-Feldern gleich true und result.document.file_id, die wir benötigen.

Prototyp


Nun, da alles fertig ist, lasst uns zur Sache kommen:

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

In diesem Skript wählen wir eine zufällige Datei aus der Liste aus, die wir im Archiv haben, suchen die LFH-Startposition, addieren die LFH-Länge dazu und erhalten so die Startposition der Daten für eine bestimmte Datei im Archiv.

Addiert man die Dateilänge dazu, so erhält man die Position des Datenendes.

Es bleibt, die Adresse der Datei selbst zu erhalten (dies ist in einem bestimmten Fall bei einem Telegramm der Fall) und dann einfach eine Anfrage mit der Überschrift Range: bytes=<->-<->

(vorausgesetzt der Server unterstützt Range Requests)

Wenn Sie auf das angegebene Skript zugreifen, sehen Sie den Inhalt eines zufälligen Bildes aus unserem Archiv.

Nein, es ist nicht ernst ...


Es versteht sich von selbst, dass die Anforderung von getFile zwischengespeichert werden sollte, da Telegramme garantieren, dass die Verbindung mindestens eine Stunde lang besteht. Im Allgemeinen ist dieses Beispiel nichts weiter als zu zeigen, dass es funktioniert.

Lass es uns etwas härter versuchen - starte alles mit Amphp. Immerhin verteilen die Leute statische Daten in der Produktion auf node.js, aber was hindert uns daran (naja, abgesehen vom gesunden Menschenverstand natürlich)? Wir haben auch hier Asynchronität, wissen Sie.

Wir brauchen 3 Pakete:

  • amphp / http-server - offensichtlich der Server, an den wir Dateien verteilen werden
  • amphp / artax - http-client, mit dem wir die Daten abrufen und einen direkten Link zur Datei im Cache aktualisieren.
  • amphp / parallel ist eine Bibliothek für Multithreading und so weiter. Aber keine Angst, wir brauchen nur SharedMemoryParcel, das uns als Im-Memory-Cache dient.

Wir setzen die notwendigen Abhängigkeiten:

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

Und schreibe das

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

Was wir als Ergebnis erhalten: Wir haben jetzt einen Cache, in dem wir einen Link zu einem Dokument für 55 Minuten speichern. Dies stellt auch sicher, dass wir einen Link nicht mehrmals hintereinander anfordern, wenn wir zum Zeitpunkt des Cache-Ablaufs viele Anfragen erhalten. Nun, all dies ist mit PHP-FPM (oder, Gott bewahre, PHP-CGI) eindeutig einfacher als readfile.

Außerdem können Sie den Amphp-Instanz-Pool erhöhen - SharedMemoryParcel weist nach seinem Namen darauf hin, dass unser Cache zwischen Prozessen / Threads hin und her wechselt. Nun, oder, wenn Sie immer noch Bedenken hinsichtlich der Zuverlässigkeit dieses Entwurfs haben, dann sollten Sie proxy_pass und nginx verwenden .

Im Allgemeinen ist die Idee alles andere als neu und sogar in den bärtigen Jahren konnten Sie mit DownloadMaster eine bestimmte Datei aus dem Zip-Archiv herunterladen, ohne das gesamte Archiv herunterzuladen. Daher ist es nicht erforderlich, die Struktur separat zu speichern, sondern es werden einige Anforderungen gespeichert. Was ist, wenn wir beispielsweise die Datei dem Benutzer im laufenden Betrieb als Proxy zur Verfügung stellen möchten, wirkt sich dies ziemlich stark auf die Geschwindigkeit der Anforderung aus.

Warum könnte dies nützlich sein? Um beispielsweise Inode zu speichern, wenn Sie zig Millionen kleiner Dateien haben und diese nicht direkt in der Datenbank speichern möchten, können Sie sie in Archiven speichern, wobei Sie den Versatz kennen, um eine einzelne Datei zu empfangen.

Oder wenn Sie Dateien aus der Ferne speichern. In einem 20-MB-Telegramm können Sie sich natürlich nicht fortbewegen, aber wer weiß, es gibt noch andere Möglichkeiten.

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


All Articles