Jak ukládáme obrázky

Mnoho webových projektů, od malých až po ty největší, potřebuje nějak pracovat s obrázky. Ty je třeba ukládat, zpracovávat a jednoduše zobrazovat. Pojďme se podívat na to, jak obrázky ukládáme u nás v Mediu. Později řeč přijde i na elegantní zobrazování uživatelům.

Řekněme, že každý obrázek (abychom o nich někde měli přehled a mohli k nim mít i nějaká metadata) je zastoupen entitou Image. Ta nejjednodušší může vypadat třeba nějak takto (pro ještě větší zjednodušení vynechávám gettery, settery a další):

Abychom vytvořili vztah mezi naší entitou uloženou v jednom úložišti (typicky v databázi) a samotným souborem obrázku uloženým v jiném úložišti (disk, cloud, co si představíte), vytvoříme si třídu ImageRepository. Takto může vypadat:

Podívejme se na dvě závislosti třídy. $fileNamingStrategy je z nich trochu záhadnější. Protože každá instance projektu (nebo, řekněme, klient) může mít jiné požadavky na pojmenování souborů, implementují si po svém rozhraní IImageFileNamingStrategy s metodou getOriginalName. Například takto:

Nebojte se, nehashujeme hesla. MD5 je tady jako hashovací algoritmus víceméně v pohodě. Aby bylo všemu učiněno zadost a nedostali jsme nedejbože duplicitní jméno souboru pro jinou instanci Image kvůli kolizi v algoritmu (oči divoce protáčející smajlík), můžeme do nezahashované části jména souboru přidat ID entity.

Dostaneme tak jméno souboru, které vypadá třeba takhle:

Závislost IFileRepository je nasnadě – o úroveň nižší repozitář, který volá funkce samotného úložiště, čtení, ukládání, zjištění existence souboru – ten nepotřebuje vědět nic o tom, že pracuje s třídou Image, a zároveň odstiňuje ImageRepository od konkrétní implementace ukládání – jeho výměnou (u nás typicky změnou jednoho řádku v konfiguraci DI kontejneru) můžeme vyměnit celé úložiště souborů – třeba z lokálního filesystému na Amazon S3.

Řekněme, že budeme obrázky ukládat do filesystému. FilesystemRepository tedy bude mít jako property nějaký kořenový adresář $rootDir (který každá instance dostane nejlépe jako parametr konstruktoru z konfigurace DI kontejneru).

Soubory na disku je třeba podle něčeho organizovat. Tisíce souborů v jednom adresáři už můžou dát strojům pěkně zabrat – hodí se nám tedy, aby soubory byly nějak rozumně rozloženy. Tady přijde ke cti MD5 začátek jména souboru – FilesystemRepository každý soubor uloží do stuktury podle prvních dvou dvouznaků jeho jména. Máme vyzkoušeno, že se tak soubory do adresářové struktury rozloží pěkně rovnoměrně. 256×256 adresářů should be enough for everyone. Soubor zmíněný výše relativně k $rootDir se uloží takto:

Podle jména souboru víme, jak ho hledat v adresářové struktuře (kdybychom se chtěli ručně podívat, kde je třeba případný problém) a na konci souboru máme ID jeho entity – to kdyby byl problém na straně modelu.

Dokonce bychom klidně mohli md5 hash zkrátit, třeba na 8 znaků, to by mělo stačit (o kolize nám už, jak jste mohli postřehnout, až tolik nejde).

Metoda save ImageRepository tedy uloží data obrázku. Podle podobného klíče bude v ImageRepository fungovat i vyzvednutí obrázku: předáme instanci Image metodě getPath a dostaneme cestu k souboru. Metoda load pak může vracet zase obrázek jako data.

Originály souborů, v našem případě obrázků, máme uložené na disku, dokážeme podle entity najít cestu k nim a dále s nimi manipulovat. V následujícím článku se podíváme na mnohem zajímavější věc, a to jak s obrázky dále manipulujeme a posíláme je uživatelům.

Související články

Zveřejněno 29.10.2014 v rubrice Programování a vývoj se štítky , , .
humpal

Matěj Humpál

Vývojář internetových aplikací. Hudebník hubou i rukama.

Google+ profil @neldorling humpal@medio.cz

Komentáře k článku

[1] Jiří Knesl | 29. 10. 2014 v 19.55

Kolik máte tříd „***FileNamingStrategy“? Protože pokud je jedinná, celkem dost kódu si ušetříte tím, že do třídy Image přesunete metodu toOriginalName pod nějakým originálnějším názvem, třeba asUniqueFilename.

[2] Michal Illich | 29. 10. 2014 v 21.08

Ty adresáře a jména souborů docela připomínají, jak byly obrázky ukládané na blog.cz/galerie.cz :)

[3] Matěj Koubík | 29. 10. 2014 v 22.29

V tomhle případě nepotřebujete aby byla hashovací funkce výpočetně náročná (tak byla md5 navržena), ale jen aby (více/méně) rovnoměrně rozmísťovala záznamy ve stavovém prostoru. Proto bych použil nějakou která je naopak rychlá. Asi by tady stačil kontrolní součet, např. crc32().

[4] Jakub Onderka | 29. 10. 2014 v 23.22

A jak řešíte zobrazování obrázků pro HDPI displeje? Nebo je to kvůli jejich zastoupení zatím zbytečné?

[3] MD5 je kryptografická hašovací funkce, tedy je navržena tak, aby byla rychlá a právě proto se nehodí pro hašování hesel. CRC32 právě není vhodná kvůli tomu, že není navržena tak, aby nevznikaly kolize.

[5] Me | 30. 10. 2014 v 0.00

Na tohle se mi osvědčilo mongo a jeho gridfs pro nenáročný projekty na traffic. Jednoduchý migrace, rychlý čtení a žádný obstrukce kolem limitů filesystemu. Prostě jednoduchý hashe pro read a write s možnou expirací v podobě indexu …

[6] Me | 30. 10. 2014 v 0.31

Vypadá to jako znovuvanález kola. Nepište exitující algoritmy, který můžete využít. Je to fakt rychlý a obsluha vydá na max 50 řádků kódu v PHP http://docs.mongodb.org/manual/core/gridfs/.

[7] Matěj Humpál | 30. 10. 2014 v 17.08

[1] V interní knihovně máme jednu popsanou výše, některé projekty ji používají, jiné mají vlastní implementaci podle svých specifických potřeb.

[2] Však by to bylo smutné, neodnést si žádnou zkušenost :)

[4] Zobrazování pro HDPI displeje zatím, pokud vím, nikde specificky neřešíme.

[8] Jan Tichý | 3. 11. 2014 v 12.40

[6] Snažíme se zbytečně nepsat nic, co už napsal někdo jiný. Jenom se trochu obávám, že jsme to u nás potřebovali dřív, než tyhle věci v použitelné podobě vznikly někde jinde nebo než se dostaly k nám.

[9] Petr Sládek | 7. 11. 2014 v 19.49

Zdravím..

Počítám že ten ImageRepository by měl tu entitu uložit i do Databáze, je tomu tak? :)

A pak bych se chtěl zeptat, ten FilesystemRepository nemá s naming strategy nic společnýho, takže když mu předám k ukládání třeba jen název 1.jpg, tak on si ho při zjištění cesty (pro ukládání/načítání/..) sám převede do md5 a vezme ty první dva a dva znaky?

[10] Matěj Humpál | 11. 11. 2014 v 10.35

[9] Petře, kdo uloží entitu do databáze je prakticky jedno. U nás se o to většinou stará fasáda nadřazené entity (třeba u obrázků nějakého představení je to fasáda pro představení PerformanceFacade) spolu s cascade persist mechanismem Doctrine. ImageRepository by to dělat mohla také.

Třídě FilesystemRepository je jedno i pojmenování a ukládání souborů – ta uloží soubor tak, jak se jí předá, tedy 1.jpg by uložila jako 1.jpg přímo do svého $rootDir. O rozhození souborů po souborovém systému se stará o úroveň vyšší vrstva, v případě článku tedy ImageRepository.

[11] Roman Hocke | 6. 2. 2015 v 17.51

Pokud se md5 dělá jen proto, aby se obrázky rovnoměrně rozložily po adresářích, nestálo by za úvahu místo toho používat např. poslední dvě dvoučíslí přímo z ID entity? Např. obrázek s ID=123456 by byl v adresáři /34/56. Rovnoměrnost je tím zaručena téměř dokonale (pokud teda ID jde po +1) a filename se nemusí skládat ze dvou částí (hash + ID), ale jen z jedné (ID).

[12] Matěj Humpál | 10. 2. 2015 v 15.01

[11] Romane, to by pro běžné projekty určitě stačilo, přestože je takové rozložení 6,5x „těsnější“, než u hexadecimálních dvojznaků.

Předpokládám, že do 4 znaků by se prvních 999 entit doplnilo nulami.

Jediné, co bych viděl jako problém, je to „pokud teda ID jde po +1“, to teoreticky nemusí být vždy splněno. Plus ukládání podle hashe používáme i na jiných místech v aplikaci pro soubory bez id entity ve jméně, takže máme rozkládání udělané všude stejně.