O webu
Upload velkých souborů v JS/PHP

U některých typů webových aplikací je potřeba umožnit uživatelům nahrávat na server soubory (obrázky, videa a podobně).

Nahrát v PHP soubor velký několik stovek kB nebo jednotek MB není příliš složité – stačí k tomu formulář a funkce move_uploaded_file.

Co ale v případě, že požadované soubory mají stovky megabytů nebo dokonce gigabyty?

Demo

Jen simulace (data se nikam neodesílají), ideální je nahrát soubor v řádu stovek MB:

Přetáhněte sem soubor

Ke stažení:

Zvýšení limitů v PHP

Mimo sdílené hostingy jde hýbat s limity pro maximální velikost souborů.

Bohužel to má některé problémy:

  1. Při selhání spojení se celý obsah zahodí a musí se přenášet znovu.
  2. Nezobrazuje se průběh nahrávání. (Jde řešit pomocí APC.)

Cesta tedy vede jinudy.

JS FileReader

Od IE 10 prohlížeče podporují FileReader. Jedná se o JavaScriptovou třídu, která umí číst soubory přímo na straně klienta.

Při uploadu menších souborů jako jsou obrázky nebo text jde obsah zobrazit přímo na stránce, aniž by se musel odesílat na server.

Teoreticky by tak šlo celé video zobrazit v prohlížeči před uploadem. V praxi je ale zobrazení velkého souboru velmi náročné na operační paměť.

Rozsekání souborů

Klíčová vlastnost pro nahrávání obrovských souborů je možnost jejich rozsekání.

var reader = new FileReader();
reader.readAsDataURL(
  soubor.slice(zacatek, konec)
);

Pomocí slice, jde ze souboru vzít libovolnou část a načíst ji jako data URL.

reader.onload = function (e) {
  var cast = e.target.result;
}

V proměnné cast nyní po načtení bude část souboru, kterou jen stačí AJAXem odeslat na server a pomocí PHP uložit.

Na tomto principu je tedy třeba postavit rekursivní funkci, která projde celý soubor po stanovených blocích (třeba 1 MB) a všechno postupně odešle na server.

Je nutné zachovat pořadí odesílaných částí, takže je potřeba funkci rekursivně volat až v úspěšném callbacku z volání AJAXu.

Vzhledem k tomu, že je možné předem zjistit velikost souboru a velikost jednoho bloku je také známa, není problém na základě toho sestavit znázornění průběhu odesílání.

Spojení souborů

Získání obsahu na straně PHP je velmi jednoduché. Nejprve je potřeba dekódovat data:

$data = str_replace("data:;base64,", "", urldecode($_POST['data']));
$data = str_replace(' ', '+', $data);
$data = base64_decode($data);

Nyní je stačí při každém spuštění skriptu připsat funkcí file_put_contents:

file_put_contents(
  "data/" . $_POST['filename'], 
  $data, 
  FILE_APPEND
);

Tímto způsobem jde i na sdíleném hostingu nahrávat velké soubory. Celý postup jde ale ještě vylepšit…

Vylepšení

Přerušení a navázání

Pokud nahrávání selže, je zbytečné, aby již odeslaná data musel návštěvník nahrávat znovu. Řešení je kromě připisování do jednoho souboru ještě ukládat jednotlivé části a při úspěšném uložení poslat ze serveru odpověď s číslem poslední části.

if (file_put_contents(
  "data/" . $_POST['filename'] . ".part" . $_POST['chunk'], 
  $data
)) {
  echo $_POST['chunk'];
}

Na straně klienta se poslední část pro daný soubor může uložit do localStorage. Díky tomu nebude případně problém v odesílání souboru navázat tam, kde se minule skončilo.

Ideální velikost dělení

K úvaze je, jakou zvolit jednotku pro dělení souboru. Při nižším objemu získá návštěvník rychlejší odezvu a živější aktualisaci odeslaných procent.

Dále v případě selhání odesílání bude zahozeno menší množství dat.

Na druhou stranu nahrávání po hodně malých částech může způsobit zdržení kvůli odezvě serveru a připojení.

Zobrazení rychlosti

Vzhledem k tomu, že je možné zaznamenat čas, kdy byla na server data odeslána, a čas, kdy byla uložena, a stejně tak je známa velikost bloku, dá se spočítat rychlost odesílání.

Podle rychlosti by potom případně šlo ovlivňovat velikost odesílaných bloků.

Metoda readAsBinaryString

Místo čtení dat metodou readAsDataURL by nejspíš bylo lepší použít readAsBinaryString. Podpora v prohlížečích je ale podle všeho horší (nefunguje v IE).