Hlavní navigace

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

Jakub Vrána 16. 5. 2005

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?

20. 5. 2005 14:22

Všiml si někdo toho, že článek byl o tom, jak se vyhnout stahování celého archivu v situaci, kdy z něj chceme získat jenom malou část?

17. 5. 2005 19:28

No, zase je fakt, ze by se to melo jmenovat spise:

"Zajimave programatorske perlicky, ktere jsou zrovna tady naprogramovany v PHP, ale jinak by mohly byt napsany v cemkoliv jinym"

To samy by slo udelat i v perlu, pythonu, C++, jave ... i kdyz na serveru ktery ten zip odnekud taha casto byva jen PHP .... takze nejaka souvislost tu precejen asi je ....



DigiZone.cz: Česká televize mění schéma ČT :D

Česká televize mění schéma ČT :D

Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

Lupa.cz: Slevové šílenství je tu. Kde nakoupit na Black Friday?

Slevové šílenství je tu. Kde nakoupit na Black Friday?

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: Vládu obejde, kvůli EET rovnou do sněmovny

Vládu obejde, kvůli EET rovnou do sněmovny

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Podnikatel.cz: Na poslední chvíli šokuje vyjímkami v EET

Na poslední chvíli šokuje vyjímkami v EET

Vitalia.cz: Tesco: Chudá rodina si koupí levné polské kuře

Tesco: Chudá rodina si koupí levné polské kuře

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Lupa.cz: Babiš: E-shopů se EET možná nebude týkat

Babiš: E-shopů se EET možná nebude týkat

Podnikatel.cz: EET zvládneme, budou horší zákony

EET zvládneme, budou horší zákony

120na80.cz: Rakovina oka. Jak ji poznáte?

Rakovina oka. Jak ji poznáte?

120na80.cz: Bojíte se encefalitidy?

Bojíte se encefalitidy?

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

120na80.cz: Pánové, pečujte o svoje přirození a prostatu

Pánové, pečujte o svoje přirození a prostatu

Vitalia.cz: Chtějí si léčit kvasinky. Lék je jen v Německu

Chtějí si léčit kvasinky. Lék je jen v Německu