Ve webových aplikacích je často potřeba uploadovat soubory z klienta na server. Buďto se tam mají nějakým způsobem zpracovat, nebo třeba jenom uložit a zpřístupnit někomu dalšímu. ASP.NET Core nabízí dvě základní metody bufferovaný a streamovaný upload.
- Bufferovaný upload je nejjednodušší na zpracování. Celý uploadovaný soubor se natáhne do paměti a je k dispozici jako
IFormFile
a mohu zjistit jeho velikost, původní název a samozřejmě přistupovat k jeho obsahu. Tato metoda je vhodná zejména pro menší soubory, ve velikosti jednotek nebo malých desítek MB. - Streamovaný upload je vhodnější pro větší soubory, protože nevyžaduje, aby se všechno načetlo do paměti najednou, ale data lze zpracovávat nebo ukládat postupně. Díky tomu lze ukládat i data o objemu stovek megabajtů až gigabajtů.
Připravil jsem pro vás dvoudílný seriál, který toto téma pojednává. K dispozici jsou i zdrojové kódy příkladu.
Bufferovaný upload a validace názvů souborů
V prvním videu vám ukážu, jak používat klasický bufferovaný upload, což je velmi jednoduché. Také zde mluvím o důležitém tématu, kterým je sanitizace názvů souborů.
File name sanitizer
Klient sice pošle s daty i název souboru (jaký název měl původně), ale těmto datům není dobré věřit. Protože zlovolný klient může podstrčit v podstatě cokoliv. Nelze tedy bez dalšího vzít název souboru zaslaný klientem a použít ho jako název souboru na serveru. Ideální z hlediska bezpečnosti by bylo, kdybychom mohli klientem zaslaný název souboru zcela ignorovat a vytvořit si vlastní název, o kterém víme, že je bezpečný. To ale často není možné.
Vyvořil jsem proto interface IFileNameSanitizer
, který má jedinou metodu SanitizeFileName
a ta dostane na vstupu data zadaná z klienta a na výstupu vrátí bezpečný název souboru:
public interface IFileNameSanitizer {
public string? SanitizeFileName(string fileName);
}
Zároveň jsem vytvořil jeho implementaci WindowsFileNameSanitizer
, což je třída, která provádí velice konzervativní ošetření názvů souborů, mimo jiné na základě pravidel zmíněných v článku Naming Files, Paths, and Namespaces. Pravidla pro Linux by byla poněkud volnější (název souboru může na Linuxu obsahovat i znaky, které by na Windows možné nebyly), ale chceme-li aby byla aplikace multiplatformní, je lepší používat konzervativnější přístup Windows.
Třída vypadá takto:
public class WindowsFileNameSanitizer : IFileNameSanitizer {
private const string ValidFileNameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.";
private static readonly string[] ReservedNames = ["^CON$", "^PRN$", "^AUX$", "^NUL$", @"^COM\d*$", @"^LPT\d*$"];
public string? SanitizeFileName(string fileName) {
// Get file name without path
fileName = Path.GetFileName(fileName);
// Replace spaces with underscores
fileName = fileName.Replace(' ', '_');
// Remove diacritic from file name
fileName = string.Join("", fileName.Normalize(NormalizationForm.FormD).Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)).Normalize(NormalizationForm.FormC);
// Remove all unsupported characters
fileName = string.Join("", fileName.Where(ValidFileNameChars.Contains));
// Shorten the file name to 64 characters and extension to 32 characters
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
var extension = Path.GetExtension(fileName);
if (fileNameWithoutExtension.Length > 64) fileNameWithoutExtension = fileNameWithoutExtension[..64];
if (extension.Length > 32) extension = extension[..32];
// Replace all dots in filename part with underscores
fileNameWithoutExtension = fileNameWithoutExtension.Replace('.', '_');
// Do not accept files whose names or extensions are empty after removing unsupported characters
if (string.IsNullOrEmpty(fileNameWithoutExtension)) return null;
if (string.IsNullOrEmpty(extension)) return null;
// Do not accept files with reserved names
foreach (var reservedName in ReservedNames) {
if (Regex.IsMatch(fileNameWithoutExtension, reservedName, RegexOptions.IgnoreCase)) return null;
}
// Join the file name and extension
return fileNameWithoutExtension + extension;
}
}
Postup sanitizace je následující:
- Z názvu souboru odstraníme cestu, pokud je přítomna.
- Případné mezery nahradíme podtržítky.
- Odstraníme diakritiku běžně používaným trikem, kdy string převedeme na Unicode Form D, kdy jsou diakritická znaménka jako samostatné znaky a ty odstraníme. Poté převedeme string na běžnější Form C. Nejsem si zcela jist, zda toto odstranění diakritiky funguje zcela spolehlivě za všech okolností, ale pro české znaky je uspokojivé.
- Odstraníme z názvu souboru všechny znaky, které nejsou v seznamu povolených. Povolená jsou pouze velká a malá písmena anglické abecedy, číslice, mínus, podtržítko a tečka.
- Název souboru zkrátíme na 64 znaků a příponu na 32 znaků. Tento limit jsem zvolil zcela odhadem, limit délky cesty závisí na použitém souborovém systému atd., ale 64 + 32 znaků mi přijde jako rozumný limit pro běžné soubory.
- Tečky v názvu souboru nahradíme podtržítky. Název souboru bude obsahovat právě jednu tečku, před příponou.
- Ověříme si, že soubor má jak název, tak příponu. Neakceptujeme tedy soubory bez přípony.
- Odmítneme soubory s názvy, které jsou na Windows rezervované, jako
CON
,PRN
,AUX
,NUL
,COMx
neboLPTx
.
Výsledek vrátíme zpět. Pokud se nepodařilo vytvořit validní název (třeba protože název souboru obsahuje pouze nepovolené znaky nebo je rezervovaný), vrátí metoda null
. Volající kód se s tím musí nějak vyrovnat, třeba vrátit chybu.
Streamovaný upload na několik souborů
Druhé video se zabývá streamovaným uploadem souborů. Tam přistupujeme k datům na mnohem nižší úrovni, nemáme k dispozici pohodlné zpracování formulářových dat a podobně. Navíc je třeba trocha klientského JavaScriptu. Výměnou za to ale dokážeme uploadovat i větší soubory a hlavně v průběhu uploadu komunikovat s klientem -- ukázat mu průběh uploadu, rychlost a podobné věci.
Ve videu ukážu postupně tři varianty uploadu, z nichž v praxi použitelná je vlastně jenom ta poslední. První dvě totiž neumějí zjistit, že se soubor v pořádku nahrál celý, pokud bude upload přerušen, zůstane soubor nekompletní. Poslední příklad ale používá dočasný soubor, pomocí jehož názvu sváže uploadovaný soubor s ostatními odeslanými formulářovými daty.
To není všechno
Obě popisované metody mají jednu velkou výhodu i nevýhodu zároveň: přenos běží v rámci jednoho HTTP requestu. Je velmi jednoduchý, ale pořád jsme omezeni maximální velikostí jednoho requestu a nedokážeme třeba navázat spojení v případě jeho výpadku a podobně.
Pokud bychom potřebovali takovou funkcionalitu, použitelnou i pro souboru o velikosti mnoha gigabajtů, bylo by zapotřebí k tomu přistupovat výrazně jinak. Pomocí JavaScriptového File API lze z browseru přistupovat k (některým, uživatelem vybraným) souborům ve file systému. Můžeme je postupně po částech načítat a postupně posílat v několika HTTP requestech na server. Nicméně to je podstatně složitější postup a jím se budu možná zabývat někdy příště, stejně jako způsoby zpracování a ukládání souborů na straně serveru.