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.

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í:

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.