Od té doby, co jsem tu před pár dny publikoval článek o atomických operacích se soubory, se hodně změnilo. Udělal jsem značný pokrok a představím vám nyní naprostou novinku.

Možná ještě krátce, proč se této problematice tolik věnuji. Obecně totiž platí, že pokud váš PHP skript vytváří nebo píše do souborů, je nutné toto řešit! Už pouhý jeden zápis je kritický! V opačném případě musíte počítat se ztrátou dat a vznikem těžko odhalitelných chyb.

Tedy věnujte tomu prosím pozornost.

Kde nestačí databáze ani NSafeFile

Určitě víte, že atomického čtení, zápisu a mazání lze dosáhnout použitím databází. Tedy místo ukládání dat ve formě souborů je uložíme jako záznamy do SQlite apod. Nepříjemné je, že třeba takový konfigurační INI soubor se jakožto záznam databáze hůř edituje.

Minule jsem tu dal k dispozici třídu, která uměla atomicky načíst, zapsat a smazat soubor (skript prošel určitým vývojem, o tom později). Pomocí ní můžeme onen INI soubor bezpečně zapsat a přečíst z disku – editace tedy bude pohodlnější.

Jenže ouha! Je tu problém. Soubor chci číst funkcí parse_ini_file(), která zámky nepoužívá. Čímž jde celá snaha o atomicitu do kytek. Databáze samozřejmě nepomůže už vůbec. Co teď?

Nette\IO\SafeStream

Trklo mě to včera večer a mám pocit, že to je fakt bomba 🙂 Ne, vážně, tohle řešení se mi prostě líbí. Nette\IO\SafeStream (dříve NSafeStream) registruje „bezpečný stream“, pomocí něhož můžeme atomicky manipulovat se soubory prostřednictvím standardních funkcí. Stačí jen uvést protokol „safe://“. Příklad:

// před jméno souboru přidáme safe://
$handle = fopen('safe://test.txt', 'x');
// ve skutečnosti se vytvořil dočasný soubor

fwrite($handle, 'La Trine');

fclose($handle);
// a teprve teď se přejmenoval na test.txt

// můžeme soubor smazat
unlink('safe://test.txt');

// a vůbec používat všechny známé funkce
file_put_contents('safe://test.txt', $content);

$ini = parse_ini_file('safe://autoload.ini');

Parádička, ne?

Jak to funguje?

V podstatě se jedná o přepis původního NSafeFile do podoby Stream Wrapper. Nette\IO\SafeFile je mimochodem několikrát překopaný skript, původní verze totiž fungovala na špatném předpokladu, na což mě v diskuzi upozornil mka. Jo, taková interakce se mi vážně líbí. Vlastně jsem rád, že tam ta chyba byla, protože se mi celou věc podařilo zvládnout bez lockfiles. Tedy je to velice výkonné.

Podívejme se na magii řešení:

Čtení

Otevřu soubor v módu r a pokusím se získat zámek pro čtení (neboli shared lock, LOCK_SH). Poté je možné soubor volně číst. Zámek se uvolní automaticky s uzavřením souboru – fclose() nebo při ukončení skriptu.

Zápis

Otevřu soubor v módu r+. Tím zjistím, zda existuje (budeme přepisovat) nebo je ho třeba vytvořit.

Zápis do existujícího souboru

Soubor je tedy otevřen v módu r+. Získáme zámek pro zápis (neboli exclusive lock, LOCK_EX). Obsah vymažeme funkcí ftruncate() a pak můžeme do souboru volně psát, až do uzavření a uvolnění zámku.

Zápis do neexistujícího souboru

Není možné vytvořit nový soubor, protože než bych získal zámek, mohl by s ním pracovat jiný thread. Proto vytvoříme dočasný (temporary) soubor v módu x a získáme exkluzivní zámek. Poté do něj volně zapisujeme. V okamžiku uzavření souboru jej zkusíme přejmenovat na požadovaný název. Pokud se přejmenování nezdaří (jiný thread mezitím tento soubor vytvořil), dočasný soubor smažeme.

Zápis v módu append

Protože není možné soubory otevírat v režimech a nebo w, neboť vytvoření nového souboru je nežádoucí akce, otevřeme jej v módu r+ a posuneme ukazatel na konec via fseek().

Mazání souboru

Soubor prostě smažeme funkcí unlink(). Že má soubor otevřený jiný thread, ať už pro čtení nebo zápis, nám nemusí vadit. Ve Windows totiž otevřený soubor vůbec smazat nelze a unlink selže. Naopak v Unixu se smaže, jakožto položka adresáře, ale s jeho obsahem je možné nadále bezpečně pracovat.

Tak a to je celé. Ono je to vlastně docela jednoduché, že? 🙂 Tak můžete testovat a připomínky i nápady jsou vítány!

Download

Nette\SafeStream