Častou součástí moderních aplikací je sdílení vlastního obsahu. Ať už se jedná o sociální sítě, chat, úložiště a trezory či objednávkové systémy, s nahráváním souborů se setkáváme téměř všude. Koneckonců jaká aplikace v dnešní době vlastně nemá uživatelský profil s vlastním avatarem? Pokud nepoužíváme externí službu na ukládání souborů, musíme se při návrhu našeho serveru zamyslet, jak nahrávání souborů budeme řešit. Jednoduchá odpověď ale možná často není ta nejlepší.
Shrnutí základní teorie
Pokud se při implementaci nahrávání souborů omezíme na HTTP protokol, standardem pro nás bude zasílání data o souborech typu multipart/form-data
pomocí metody POST. To umožňuje zasílání dat o souborech z klienta na server po částech. V případě, že se jedná o formulář na webu, práci za nás často odvádí formulářový prvek v kombinaci s prohlížečem sám.
Nezáleží, zda-li server vyvíjíme pro cloud nebo vlastní infrastrukturu, nejlepší praktikou je soubory ukládat mimo na externí úložiště přizpůsobené k pojmutí velkého množství dat, jejich zabezpečení a verzování. Zabrání se tak nutnosti škálování disků serveru, decentralizace a zálohování. Mezi nejpoužívanější cloudové úložiště patří například Google Cloud Storage či Amazon S3.
Soubory společně s ostatními daty – proč ne
Zřejmě nejjednoduší a naprosto zřejmou variantou řešení nahrávání souborů je jeho zakomponování přímo do vstupu obsahující i jiná data. Pokud uživatel vyplňuje formulář, odešle společně s daty i soubor. Tím celý požadavek získá typ multipart/form-data
a každá část formuláře pak bude muset být na serveru zpracována zvlášť. Naštěstí dnes existuje velké množství knihoven a MVC frameworků, které práci s parsováním těchto requestů pomohou, díky čemuž můžeme s těmito daty pracovat velmi jednoduše.
Pokud pracujeme s RESTful API, je pro nás implementace snadná. V případě GraphQL musíme využít neoficiální konvence pro nahrávání souboru a definici vlastního skaláru, jelikož nahrávání souboru není součástí standardu. Z pohledu čistoty kódu se jedná o elegantní řešení, které využívá přednosti GraphQL schéma. Definice vlastního skaláru je dobře čitelná i jednoduše použitelná v resolveru.
Na druhou stranu vzhledem k tomu, že standardním datovým typem přijímaným většinou běžných GraphQL knihoven jako například Apollo server či graphql-yoga je application/json
, jednoduchá integrace tohoto standardu může znamenat CSRF zranitelnost pro vaši aplikaci. Nezapomeňte proto ověřit, že se vás daná zranitelnost netýká, případně přidat externí CSRF ochranu (Apollo od verze 3 má vlastní csrf ochranu).
Výhodou tohoto přístupu je navázání na zbytek dat, ke kterým se nahraný soubor váže. Pokud například přikládáme k objednávce fotografii, můžeme zaručit, že objednávka bude validní jen v případě, že i zaslaná fotografie je v pořádku.
Nicméně nevýhodou je výkonnost tohoto řešení. Server musí kromě úkonů nad daty (uložení do databáze, validace atd.) pracovat i s přesunutím streamovaných dat z klienta na externí úložiště, jejich validací, parsováním a opětovným serializováním do dalšího requestu. Server je tak zatížen přesměrováním velkého objemu dat, ač se jedná o jeden jediný požadavek. V případě, že náš formulář dokonce umožňuje nahrát i více souborů, výkonnost tohoto řešení bude silně záležet na výkonnosti našeho serveru.
Jeden endpoint vládne všem
Předchozí řešení je samozřejmě možné vylepšit pomocí jednoho endpointu (REST), mutace (GraphQL), který bude vyhrazen jen pro nahrávání souborů. Klient tak nahrává soubor na jednom místě a zbytek vstupu pak odesílá na místo druhé. V případě, že se chceme vyhnout zmíněné GraphQL konvenci pro nahrávání souborů, můžeme tímto způsobem využít i RESTful API endpoint pro GraphQL.
Výhodou je, že nad separovaným nahráním souboru je jednoduché sledovat na klientských aplikacích postup nahrávání. Každý request může nést vlastní Content-Type
, což může být výhoda například při sledování access logů. V případě, že dojde na chybu při zpracovávání požadavku (například z důvodu nedostupnosti externího úložiště), nedojde ke ztrátě celého požadavku. Velkou výhodou také může být jednoduchost implementace a uniformní řešení i pro klientské aplikace ve všech formulářích a možnost oddělení tohoto endpointu jako mikroslužby.
Výkonnost takového řešení ale nebude o moc lepší než řešení původního. Server stále musí data přijmout a přesměrovat je na externí úložiště. Navíc nyní vyvstává další problém – provázání nahraného obrázku a ostatních dat vstupu, případné zpřístupnění nahrání obrázku pouze v případě, že zbytek vstupu je validní. Pokud tento krok nezařídíme, může naše úložiště kdokoliv zahltit soubory, které nemají žádnou relaci k ostatním datům. Řešením mohou být počítané reference souborů v databázi, případně zpřístupnění nahrání souboru až ve chvíli, kdy byla zvalidována data.
Podepsaná URL
Z předchozích řešení je jasné, že využívat server jako prostředníka mezi klientem a cloudovým úložištěm není optimální. Naštěstí existuje i řešení v podobě podepsaných URL, kterými lze nahrávat soubory přímo z klienta na úložiště bez porušení bezpečnostních zásad aplikace. Podepsaná URL jsou dostupná jak pro Google Cloud Storage tak pro Amazon S3.
Aplikace může pomocí administrátorského přístupu ze serveru vygenerovat URL, na které lze zaslat multi-part request v předdefinovaném limitovaném čase. Klient URL využije a nahraje tak soubor přímo na bucket. Výkonnost řešení tak není závislá na našem serveru ale pouze na internetovém připojení klienta a dostupnosti cloudové služby.
Garanci a validaci vstupu je možné zařídit separováním dat a souborů do dvou kroků. Podepsaná URL server vydá až po zaslání zbytku dat klientem, ke kterým se soubor váže a jejich kontrole. Otázkou zůstává, jak může server ověřit, že soubor byl v pořádku na danou adresu nahrán. K tomu nám poslouží Google Cloud funkce společně s Google Storage triggery případně AWS lambda s reakcí na S3 event. Tyto jednoduché funkce přijmou událost o nahrání souboru z námi definovaného bucketu. Sloužit naší aplikaci mohou například jako registry webhooků, kam daný event přesměrují, což z nich dělá znovu použitelné jednotky napříč různými aplikacemi.
Implementace tohoto řešení je ze všech tří nejnáročnější, důležité ovšem je jeho znovupoužitelnost a univerzálnost pro jakoukoliv serverovou technologii, jelikož se dá velmi jednoduše přenést na REST, nebo i gRPC.
GraphQL: Závěr
Možností jak implementovat nahrávání souborů na server je několik a záleží na tom, jak je se soubory zacházeno. Nezapomínejme ale na správný modulární návrh, konkrétně na principy oddělení odpovědností (SoC) a KISS. Serverová GraphQL aplikace by nahrávání souborů klientem na cloudové úložiště měla přece jen nechat na cloudovém úložišti a klientovi. V případě, že nevyužíváte cloudové úložiště, nebo nepodporuje podepsaná URL, použití RESTového endpointu či zakomponování konvence pro nahrávání souborů pro GraphQL se nejspíš nevyhnete.