Článek vyšel v rámci PHP okénka na serveru Root.cz.
Už se mi několikrát stalo, že jsem musel stahovat mnohamegový ZIP archiv jenom proto, že jsem z něj následně potřeboval získat jeden soubor. Vzhledem k pevné struktuře ZIPu by ale získání jednoho souboru mělo být možné i bez stahování celého archivu. Pokud by PHP rozšíření ZIP umělo pracovat se vzdálenými soubory, možná by se problém dal vyřešit snadno – nicméně předpokládám, že by se archiv stejně vždycky nejprve celý stáhl a pak by se s ním teprve pracovalo. Řešení tedy bude muset jít poměrně hluboko:
Postup bude samozřejmě fungovat jen u HTTP serverů s podporou stahování částí souborů. Pokud bude v archivu uložena spousta malých souborů, nepřinese postup žádnou časovou úsporu – vyplatí se tedy jen u archivů, kde stažení průměrně velkého souboru zabere víc času než položení nového HTTP požadavku.
Situaci máme trochu zkomplikovanou tím, že PHP zatím uznává jako úspěšný návratový kód pouze 200 OK, takže 206 Partial Content považuje za chybu. Proto nemůžeme použít kontexty a k serveru musíme přistupovat nízkoúrovňově funkcí fsockopen:
<?php /** Získání části souboru protokolem HTTP * @param string adresa souboru * @param int začátek části počítaný od 0 * @param int délka části * @return odpovídající část souboru nebo "", pokud server nepodporuje stahování částí * @copyright Jakub Vrána, https://php.vrana.cz/ */ function http_get_part($url, $from, $length) { $url = parse_url($url); $fp = fsockopen(($url["scheme"] == "https" ? "ssl://" : "") . $url["host"], ($url["scheme"] == "https" ? 443 : 80)); fwrite($fp, "GET $url[path]" . (isset($url["query"]) ? "?$url[query]" : "") . " HTTP/1.1\r\n"); fwrite($fp, "Host: $url[host]\r\n"); fwrite($fp, "Connection: close\r\n"); fwrite($fp, "Range: bytes=$from-" . ($from + $length - 1) . "\r\n"); fwrite($fp, "\r\n"); $status = fgets($fp); $return = ""; if (preg_match('~^HTTP/[^ ]+ 206~', $status)) { while ("\r\n" != fgets($fp)) { // přeskočení hlaviček } while (strlen($return) < $length && ($s = fread($fp, $length))) { $return .= $s; } } fclose($fp); return $return; } ?>
Funkce fsockopen nám dovoluje se serverem komunikovat přímo na úrovni protokolu HTTP – serveru pošleme dotaz (např. GET / HTTP/1.1
), hlavičky (název: hodnota), prázdný řádek a případné tělo (u metody POST) a on nám vrátí stav (např. HTTP/1.1 206 Partial Content
), hlavičky, prázdný řádek a tělo. Server může data poslat v kódování chunked, pro jednoduchost ale předpokládejme, že to neudělá – obvyklé to je u souborů, u kterých není dopředu známá velikost vracených dat (např. výstup z PHP skriptu).
Dalším krokem je postupné procházení archivu po jednotlivých souborech. Z popisu formátu jsme se dozvěděli, že hlavička každého uloženého souboru musí začínat pevným řetězcem, délka názvu souboru je uložena na pozici 26-27, délka zkomprimovaných dat na 18-21 a délka dodatečných hlaviček na 28-29. Na základě těchto informací můžeme načítat názvy souborů a přeskakovat nezajímavé soubory.
<?php /** Získání vybraných souborů ze ZIP archivu dostupného protokolem HTTP * @param string adresa ZIP archivu * @param array soubory, které chceme získat - může být i řetězec s jedním souborem * @return string ZIP archiv s požadovanými soubory * @copyright Jakub Vrána, https://php.vrana.cz/ */ function http_get_files_from_zip($url, $files) { static $header_len = 30; static $max_name_len = 256; if (!is_array($files)) { $files = array($files); } $return = ""; $from = 0; // aktuální pozice v archivu while (substr(($part = http_get_part($url, $from, $header_len + $max_name_len)), 0, 4) == "PK" . chr(3) . chr(4)) { $lengths = unpack("Vsize/vname/vextra", substr($part, 18, 4) . substr($part, 26, 4)); if (in_array(basename(substr($part, $header_len, $lengths["name"])), $files)) { $return .= http_get_part($url, $from, $header_len + array_sum($lengths)); } $from += $header_len + array_sum($lengths); } return $return; } ?>
Funkce prochází archivem, načítá názvy souborů a pokud byl soubor požadován, tak ho načte. Pokud server nepodporuje stahování částí souborů a funkce http_get_part
tedy vrátí prázdný řetězec, tak while cyklus ihned skončí a funkce vrátí prázdný řetězec. Pro zjištění velikostí uložených v hlavičce každého souboru je použita funkce unpack.
Kód je nakonec poměrně krátký, byť vyžadoval rozličné znalosti. Kdyby PHP uznávalo kód 206 za úspěšný, smrskla by se navíc funkce http_get_part
do dvou řádek:
<?php function http_get_part_context($url, $from, $length) { $context = stream_context_create(array('http' => array('header' => "Range: bytes=$from-" . ($from + $length - 1)))); return file_get_contents($url, false, $context); } ?>
Na závěr nezbytná ukázka: získání souboru php.ini-dist
z PHP verze 4.3.0:
<?php $url = "http://museum.php.net/win32/php-4.3.0-Win32.zip"; file_put_contents("php430.ini-dist.zip", http_get_files_from_zip($url, "php.ini-dist")); ?>
Diskuse je zrušena z důvodu spamu.