Získání souboru ze ZIP archivu

Školení, která pořádám

Č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:

  1. Z 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 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"));
?>
Jakub Vrána, Řešení problému, 16.5.2005, diskuse: 3 (nové: 0)

Diskuse

Martin:

Potreboval bych vyresit ponekud trivialnejsi problem nez je zde popsan presto si s nim nevim rady. Pokousim se otevriz zip archiv na vzdalenem serveru ale nedari se mi to ponevadz funkce zip_open mi hlasi chybu. Jedine co se zipem potrebuji udelat je cely ho precist(obshuje nekolik XML souboru) a nektere informace ulozit do DB, zadne ukladani soubouru na disk a neco podobneho. Kdyz jsem zkousel tahat soubour z lokalu tak bylo vse OK ale kdyz jsem se o stejne pokusil ze vzdaleneho serveru zip_open nahlasila chybu.

Martin

ikona Marty:

Nevi nekdo o tride pro praci s zip archivy? Na webhostingu je standardne vypnuto rozsireni ZIP a zapnout 'nelze'.

Nasel jsem napr. tyto:
http://www.phpclasses.org/browse/file/10489.html
http://www.zend.com/zend/spotlight/creating-zip-files1.php

.. vsechny vsak maji nejakou nevyhodu, napr. neumeji extrakci souboru, komprese funguje, ale pri vlozeni do PHPExcel projektu pak dojde k tomu, ze Excel zahlasi chybu, ktera je ale opravitelna.

Dekuji predem

ikona Ivo:

vyzkoušej tohle - http://www.phpconcept.net/pclzip/index.en.php

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.