Vytvoření a poslání velkého ZIP archivu

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

Do webového rozhraní jsem potřeboval umístit odkaz pro stažení velkého množství fotek. Když by to bylo určeno pro technicky orientované uživatele, tak bych jim to šoupnul do TAR archivu, který má triviální strukturu a snadno se vytváří za běhu (není potřeba průběžně shromažďovat žádné informace). Tento archiv nepodporuje kompresi, to ale u fotek nevadí, protože ty už jsou komprimované samy o sobě. Běžní uživatelé si s tímto formátem ale neporadí a potřebují ZIP.

V PHP je extenze ZIP, která je šikovná na vytváření malých archivů, pro velké se ale vůbec nehodí. Archiv lze vytvářet v zásadě třemi způsoby:

  1. Všechna data shromažďovat v paměti, na konci je uložit na disk a poslat.
  2. Data průběžně odkládat na disk, na konci je poslat.
  3. Data průběžně posílat.

Extenze ZIP patří do první kategorie, my bychom ideálně potřebovali něco z třetí kategorie. Druhá kategorie je nepříjemná v tom, že dokud není práce hotova, tak uživatel nemá žádnou odezvu. Navíc pokud má archiv několik giga, tak může být i problém místo na disku, obzvlášť pokud archivy stahuje víc lidí najednou.

Odolal jsem pokušení nastudovat si formát ZIP a vytvářet si ho sám a sáhl jsem po hotové knihovně. Chvíli jsem toho litoval, ale nakonec to dobře dopadlo. Nejprve jsem vyzkoušel knihovnu ZipStream-PHP, se kterou se docela dobře pracuje, ale vytvořený archiv nedokáže rozbalit Finder na Macu. Když zjistí, že archiv nedokáže rozbalit, tak neohlásí žádnou chybu a místo toho ho podruhé zabalí – uživatelsky přívětivé! Kód jsem proto přepsal pro knihovnu PHPZip, se kterou se taky dobře pracuje a archiv šlo rozbalit všude.

<?php
include 'ZipStream.php';
$zip = new ZipStream("$archive.zip");
$compress = false;

// output buffering by naše úsilí o postupné posílání zhatil
while (ob_get_level()) {
    ob_end_clean();
}

foreach (glob("files/*") as $filename) {
    $file = file_get_contents($filename);
    $zip->addFile($file, $filename, filemtime($filename), null, $compress);
}

$zip->finalize();
?>

Za hlavní možnost vylepšení tohoto kódu považuji možnost navázat přerušené stahování hlavičkou Range. Je velmi frustrující, když stáhnete giga fotek a kvůli nějakému zakuckání je musíte stahovat znovu. Vyžadovalo by to mít někde dostupnou velikost souborů bez nutnosti je stahovat (což obvykle není problém), vypnout kompresi (což u fotek nevadí) a vědět, jaká je velikost hlaviček vytvářených knihovnou.

Dalším vylepšením by bylo posílat hlavičku Content-Length, aby uživatel věděl, kolik procent už stáhl. To by navíc ještě vyžadovalo vědět, jak velký bude adresář umísťovaný na konec archivu.

Fotky jsou v mém případě umístěny v úložišti S3, což obnáší ještě jednu nepříjemnost – musíme je tam odtud přenášet na webový server (což je rychlé a ničemu to nevadí) a z webového serveru ke klientovi (což může být pomalejší než z S3 a může to být dražší). Ideální by bylo, když by možnost stažení více souborů najednou podporovalo přímo S3. Pokud bychom dopředu věděli, které fotky budeme stahovat pohromadě, tak bychom si archiv mohli do S3 uložit, to ale nebyl můj případ.

Jakub Vrána, Řešení problému, 9.8.2013, diskuse: 9 (nové: 0)

Diskuse

Milsa:

To úvodné include je nedopatrením ponechaný pôvodný kód? Nemá tam byť PHPZip?

Petr:

Ne nemá, https://github.com/Grandt/PHPZip/blob/master….Example1.php

xaxofon:

nejaky link na ten upraveny kod, resp. rozdiely voci ZipStream ... ?

ikona kočka na péro:

Ty délky hlaviček jsou popsané zde: http://www.onicos.com/staff/iz/formats/zip.html
Možná by stačilo při navazování sečítat velikosti souborů + velikosti souvisejících hlaviček a až se dojde k hodnotě range, tak obrázek do archivu přidat a pomocí output bufferingu oříznout na správném místě.
Jenže takhle se možná bude lišit konec archivu.

Jan Prachař:

V životě by mě nenapadlo, že bude mít někdo problém s formátem TAR.

george:

asi moc nepřicházíte do styku s Běžným Frantou uživatelem

Franta:

BFU v GNU/Linuxu nebo BFU v Mac OS prostě jen klikne a otevře se mu archiv. V čem je problém?

David Grudl:

A co udělá zbývajících 90 % BFU?

Vladimír Pilný:

Před časem jsem potřeboval také on-the-fly generování ZIPů a taktéž jsem skončil u něčeho jako varianty ZipStream. Překvapivě ale všechny tyto knihovny používají při volbě komprese gzcompress() + odstranění CRC hlavičky. Tudíž je nepůjde použít pro velké soubory. A i kdyby, tak to nebude praktické, protože než se na serveru soubor celý zkomprimuje, klientovi se nic nemůže poslat.
Skoro to vypadá, jako by pro PHP neexistovalo řešení pro skutečné streamované ZIPy (s volitelnou Data description (C.) sekcí po komprimovaných datech). Jedině při té vypnuté kompresi.

Jinak, Content-length by šel teoreticky správně vyplnit i při nastavené kompresi. Stačilo by mít u souborů poznamenané i komprimované velikosti. U mě to narazilo ale zase na eventualitu, že se teprve v průběhu ZipStreamu může zjistit, že některé požadované soubory jsou na momentálně nedostupném serveru a tak v archívu budou chybět. Takže také nic.

Vložit příspěvek

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-2016 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.