Autor Zpráva
MONTYCEK
Profil
Zdravím, mám tu takový problém při generování náhledů fotografií v php.
Mám skript nahled.php pro vytvoření náhledu a vkládám ho přímo do tagu img zhruba takto.

<img src="nahled.php?obrazek=obrazky-uzivatelu/obrazek001.jpg&sirka=640&vyska480">

Skript pak kontroluje zda existuje požadovaný soubor s tímto náhledem (obrazek001-640x480.jpg).
Pokud existuje, tak se ihned načte a pokud ne, tak se pomocí (imagecreatefromjpeg, imagecreatetruecolor, imagecopyresampled atd.) vygeneruje pro příští použití.
Toto funguje celkem dobře, ale občas se mi stane, že se vygeneruje něco takového.



Nevěděl by tu někdo čím to může být způsobené?
juriad
Profil
MONTYCEK:
Soubor se teprve vytváří. Ulož ho do jiného souboru (náhodně pojmenovaného) a až jej dogeneruješ (skončí příkaz uložení do souboru), tak jej přejmenuješ na správný. To zaručí, že jej nikdo nenajde, dokud nebude úplně hotový.
MONTYCEK
Profil
juriad:
No nejsem si jistý jestli tomu rozumím, tak ještě přidám příklad jak to přibližně mám řešené.

<?php  
  $novy_nazev_obrazku = 'obrazek_'.$sirka.'x'.$vyska.'.jpg'.  
  if( !file_exists($novy_nazev_obrazku) ){
    // skript pro vygenerovani nahledu
  }  
  // zobrazeni nahledu
?>

Jak bych to tam měl tedy upravit?
juriad
Profil
MONTYCEK:
Toto je správně. Jako poslední řadku v // skript pro vygenerovani nahledu máš uložení náhledu do obrázku. Neukládej jej do $novy_nazev_obrazku, ale do nějakého jiného. A hned ho pak přejmenuj na $novy_nazev_obrazku.
MONTYCEK
Profil
juriad:
Aha takže vlastně nějak takto?

<?php  
  $novy_nazev_obrazku = 'obrazek_'.$sirka.'x'.$vyska.'.jpg'.  
  if( !file_exists($novy_nazev_obrazku) ){
    // skript pro vygenerovani nahledu
    // ulozeni například do souboru temp_ $novy_nazev_obrazku
    // přejmenování na $novy_nazev_obrazku
  }  
  // zobrazeni nahledu
?>
juriad
Profil
Ano. Ale pozor na to, že ten tempový název musí být unikátní (protože několik uživatelů může genrovat stejný obrázek). A před přejmenováním zkontroluješ, zda se mezitím neobjevil.
MONTYCEK
Profil
juriad:
Tak díky. Vyzkouším to.
MONTYCEK
Profil
juriad:
Tak jsem to vyzkoušel a zdálo se, že chyba zmizela, ale pak se tam znovu objevila fotografie se stejným problémem. Pravděpodobně se jedná o problém pouze u zpracování větších obrázků, protože ten u kterého se to objevilo měl velikost 1,5 Mb.
Na začátku skriptu se ještě vytváří menší kopie obrázku. Další náhledy se pak vytvářejí také chybně, jelikož už se pracuje s tou kopií.
Keeehi
Profil
U toho problémového obrázku se to děje pokaždé nebo jen občas?
MONTYCEK
Profil
Keeehi:
No právě jen občas.
mimochodec
Profil
MONTYCEK:
Opravdu to chceš mít tak variabilní, aby to vytvořilo každý zadaný rozměr, tedy třeba i obrazek001.jpg&sirka=6400&vyska4800? Nebylo by lepší těch variant stanovit třeba pět, vytvořit je hned po nahrání a při zobrazení už je mít hotové?
MONTYCEK
Profil
mimochodec:
No já myslím že bohatě by stačilo vyřešit to prvotní vytvoření kopie. Ty ostatní náhledy se vytváří bez problému pokud se vytvoří ta menší kopie.
mimochodec
Profil
MONTYCEK:
Tomu úplně nerozumím. Čím zásadním se první kopie liší od adresy s libovolně zadanými sirka a vyska? Kromě toho neodpovídáš na to, jak mi zabráníš říct si o obrázek s nějakými brutálními rozměry a ten skript ti shodit.
MONTYCEK
Profil
mimochodec:
1. někdy potřebuji nahled 100x100 podruhé přesně 150x100 a toto se občas mění, takže bych vždy dodatečně musel vytvářet nové, proto mi přijde to řešení s tím skriptem kterému se zadá potřebný rozměr celkem vhodné

2. říct o brutálně velký obrázek si nemůžeš jelikož je to omezeno na maximální velikost originálu, proto ještě vytvořím kopii se kterou se poté pracuje takže maximálně může obrázek mít velikost 900x900px

3. ve skutečnosti url ve formátu obrazek.php?obr=slozka/obrazek.jpg&sirka=600&vyska=400 nevidíš, ale zobrazí se ti něco jako www.domena.tdl/photo/nejaky-nazev-obrazku.jpg
aDAm
Profil
jen tak mimo....není dobré nemít ošetřeno jaké velikosti ti to může generovat, otevíráš tak díru pro zahlcení serveru generováním obrázků ve všech rozměrech co koho napadne.
MONTYCEK
Profil
aDAm:
Ale toto ošetřeno mám. Uživatel si nemuže sám navolit jakou velikost obrázek bude mít.
Pro příklad je zapis echo obrazek('nahled.php?obr=obrazek.jpg&sirka=100&vyska=100') a výstup je fotografie/blablabla.jpg
Keeehi
Profil
To mi ale přece nezabrání poslat požadavek na script nahled.php. Je pravda, že jako útočník bych to musel odhadnout ale u určitých útoků se dá zjistit i tato informace. Štěstí je, že se ten útok za nějakou dobu vyčerpá. Resp. po 810 000 * počet nahraných obrázků. Což ale při pouhé tisícovce zdrojových souborů vytvoří téměř miliardu kopií z čehož nebude souborový systém zrovna šťastný. No a je taky možné, že by tam mohl být problém s obrázky z jiných zdrojů. Jako třeba nahled.php?obr=http://example.com/cizi-obrazek.jpg&sirka=100&vyska=100
juriad
Profil
Všichni:
Mohli byste začít řešit původní problém (proč jsou obrázky poškozené) namísto možný či nemožných útoků? MONTYCEK již byl varován a bude-li chtít, jistě se zeptá na zabezpečení až svůj nejpalčivější (ze svého pohledu) problém vyřeší.

MONTYCEK:
Za chvíli si udělám čas a popíši správné řešení, které bude fungovat.
MONTYCEK
Profil
Keeehi:
Jakmile proměnná obr obsahuje na začátku http tak je ignorována a skript se ukončí. Ta proměnná musí obsahovat pouze jednu povolenou hodnotu na začátku, která je název té složky a to si myslím nejde tak snadno zjistit.
juriad
Profil
<?php

function getOrCreateImg($original, $w, $h) {
    $small = $original . '-' . $w . '-' . $h . '.jpg';
    $tries = 0;

    # if the small image doesn't exist yet
    while (! file_exists($small)) {
        $lock = $small . '.lock';

        # if $lock dir exists but it is too old (more than 5 seconds; adjust this constant)
        if (is_dir($lock) && time() - filemtime($lock) > 5) {
            $del = $small . '.del.lock';
            # lock the $del to remove safely $lock
            if (mkdir($del)) {
                # check once more as we have lock
                if (is_dir($lock) && time() - filemtime($lock) > 5) {
                    # we will remove $lock
                    rmdir($lock);
                    # someone will create it again; it may as well be us
                }
                # unlock $del
                rmdir($del);
            } else {
                # somebody else is removing $lock, that is fine; we will continue
            }
        }

        # try to lock $lock
        if (mkdir($lock)) {
            # check once more that $small does not exist
            if (! file_exists($small)) {
                $tmp = $small . '.tmp';

                $img = open_img($original);
                # some long calculation here

                # save the image into $tmp file not to $small
                save_img_to($img, $tmp);

                # "create" $small "at once"
                rename($tmp, $small);
            }
            # unlock $lock
            rmdir($lock);
        } else {
            # somebody else is creating the small image; we will wait
            $start = time();
            while (!file_exists($small)) {
                usleep(100 * 1000); // 100ms
                # if we waited for more than 5 seconds
                if (time() - $start > 5) {
                    # try again; the next time it may be us who will generate the image
                    break;
                }
            }
        }

        # increment number of tries
        $tries++;
        # give up after 3 tries (15 seconds in total)
        if ($tries > 3) {
            header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500);
            exit();
        }
    }

    # send the $small image which now exists
    readfile($small);
}

Tento skript (netestoval jsem jej) by měl umožnit generování obrázků, které nebudou porušené. Řeší i problém se selháním, které předpokládá, že generování nikdy nebude trvat déle než 5 sekund. V případě selhání se restartuje a to až třikrát. Samozřejmě, že ten selhavší vrátí chybu, ale další se pokusí vygenerovat obrázek znovu. Pokud jsem na nic nezapomněl, tak by kód měl být bezpečný proti jakýmkoli race-condition a měl by v teorii fungovat při libovolném počtu procesů snažících se o získání téhož obrázku.

Dost tam používám mkdir a rmdir pro zamykání. To je běžný postup, protože mkdir vrátí true, pokud se mu podařilo adresář vytvořit; pokud existoval vrátí false. Další trik spočívá v uložení obrázku do $tmp a ten následně přejmenovat (to je také atomická operace). A ta šaškárna s $del je tam kvůli tomu, aby nikdo omylem neodemknul.

Je to dost náročný kus kódu na přečtení a ještě více na vymyšlení a ještě více na otestování.
A jak říká Donald Knuth: „Beware of bugs in the above code; I have only proved it correct, not tried it.
MONTYCEK
Profil
juriad:
díky, zítra to otestuji a uvidi se.
MONTYCEK
Profil
juriad:
No zdá se mi to celkem složité, tak jsem ještě vyzkoušel řešení, že obrázek se po nahrání na server zkopíruje kam má a zároveň se vytvoří ještě jeho (menší) kopie a až poté se zapíše do DB. Pokud to nepomůže, tak vyzkouším ten kód
juriad
Profil
MONTYCEK:
Je to složité, ale jednodušeji to nejde. Musíš uvažovat o tom, že se ti několik požadavků najednou tu funkci.

Musíš si rozmyslet, co se stane když jeden proces právě ukládá obrázek do souboru a druhý jej právě čte. => Chyba, předpokládám, že to je důvod toho obrázku v [#1].
Co když dva procesy začnou generovat obrázek a pak je oba uloží do stejného souboru (budou se přepisovat navzájem).
Z tohoto vyplývá, že je nutné zamknout určitou část kódu, aby ji nemohl vykonat nikdo jiný (k tomu jsem použil mkdir).
To zase vede k problému, že generování může selhat a zámek zůstane zamčeny do nekonečna, tedy všechny ostatní procesy budou do nekonečna čekat.
To vede k detekci chyb (generování trvá déle než 5 sekund) a je potřeba odstraňovat staré zámky.
Ale co když dva procesy budou chtít odstranit ten samý zámek? A co když mezi nimi stihne ještě někdo jiný zamknout? Celé by se to rozpadlo.
Co když skript čeká příliš dlouho, protože generování jiným procesem selhalo a žádný jiný proces se o to nepokouší?
Měl by to zkusit sám. Proto tam je ten vnější while.

Celé je to jen logická úvaha a analýza každé možné situace, kde se můžou procesy v kódu nacházet ve stejný okamžik.
MONTYCEK
Profil
juriad:
Ale pokud je problém v tom, že by byl ten proces spuštěn vícekrát, tak co kdyby se to řešilo například tím že uživatel který obrázek nahraje jej uvidí jako první a ostatní třeba až po 5 minutách? (teď myslím úpravu SQL dotazu, kde by bylo uvedeno že příspěvky, které se zobrazí musejí byt starší například víc jak 5 minut)
juriad
Profil
MONTYCEK:
Ano, existují řešení, které nepřipustí generování obrázku jen tak nějakým uživatelem. Obrázek musí být vygenerovaný buď při uploadu, nebo CRONem, nebo při prvním requestu autora, nebo nějak jinak, jak vymyslíš.
MONTYCEK
Profil
juriad:
No vzhledem k tomu, že u menších náhledů problém nevzniká, tak třeba to zmenšení přímo u uploadu pomůže.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: