Загрузка и скачивание файлов, стриминг
↑ 3.3 Web API: контроллеры, Minimal API, валидация · ← Предыдущая
Зачем это на собесе
Практический вопрос: как принять/отдать большой файл и не «съесть» память.
Объяснение
Загрузка
app.MapPost("/upload", async (IFormFile file, IStorage storage, CancellationToken ct) =>
{
if (file.Length > 20_000_000) return Results.BadRequest("too big");
await using var s = file.OpenReadStream();
var key = await storage.SaveAsync(s, file.ContentType, ct);
return Results.Ok(new { key });
}).DisableAntiforgery();Для больших файлов IFormFile буферизует (в память до 64 КБ, потом во временный файл). Для стриминга без буфера читают HttpContext.Request.Body или MultipartReader.
Скачивание
app.MapGet("/files/{key}", async (string key, IStorage storage, CancellationToken ct) =>
{
var (stream, contentType, name) = await storage.OpenAsync(key, ct);
return Results.File(stream, contentType, name, enableRangeProcessing: true);
});enableRangeProcessing включает Range-запросы (докачка, видео). Для отдачи из объектного хранилища (MinIO/S3) лучше выдавать pre-signed URL, чтобы файл шёл мимо приложения.
Стриминг данных
app.MapGet("/export", async (IAsyncEnumerable<Row> rows) => Results.Ok(rows)); // JSON-массив потоком
app.MapGet("/events", (CancellationToken ct) => TypedResults.ServerSentEvents(Ticks(ct)));Нюансы и подводные камни
- Лимиты:
Kestrel MaxRequestBodySize,FormOptions.MultipartBodyLengthLimit, лимиты proxy (nginxclient_max_body_size). - Проверяйте тип файла по содержимому (magic bytes), а не по расширению/Content-Type.
- Не используйте имя файла клиента как путь (path traversal).
- Антивирусная проверка и карантин для пользовательских загрузок.
- Не загружайте файл целиком в
byte[]: OOM на больших файлах.
Практика
- Реализуйте загрузку 1 ГБ файла потоком с постоянным потреблением памяти.
- Отдайте pre-signed URL из MinIO вместо проксирования.
- Добавьте поддержку
Rangeи проверьте докачкуcurl -C -.
Вопросы с ответами
Как принять большой файл, не исчерпав память?
Читать поток порциями (
MultipartReader/Request.Body) и сразу писать в хранилище, увеличив лимиты сервера.
Зачем pre-signed URL?
Клиент загружает/скачивает файл напрямую из хранилища, снимая нагрузку с приложения.
Как защититься от path traversal при сохранении?
Генерировать собственное имя (GUID), не подставлять клиентские значения в пути.