Hlavní navigace

PHP okénko: Získání souboru ze ZIP archivu

Jakub Vrána

Dnešní PHP okénko ukazuje na příkladu získání souboru ze ZIP archivu dostupného protokolem HTTP nízkoúrovňovou práci s tímto protokolem a způsob zacházení s binárními daty.

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:

  1. popisu formátu ZIP zjistíme, jak jsou v archivu uloženy jednotlivé soubory.
  2. Pomocí HTTP hlavičky Range budeme z archivu získávat jednotlivé kousky.
  3. Požadované soubory si zkopírujeme do vlastního archivu, který půjde následně rozbalit standardními prostředky.

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 $url adresa souboru
@param int $from začátek části počítaný od 0
@param int $length délka části
@return odpovídající část souboru nebo "", pokud server nepodporuje stahování částí
*/
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, "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 $url adresa ZIP archivu
@param array $files 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
*/
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, 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"));
?> 

Podobně laděné texty můžete najít i na autorově weblogu PHP triky.

Našli jste v článku chybu?