using foodmarket.Api.Storage; using Microsoft.AspNetCore.Mvc; namespace foodmarket.Api.Controllers.Uploads; /// Универсальный stream-endpoint для отдачи объектов из /// IObjectStorage. Маршрут /uploads/{**path}. Для Local — читает с /// диска, для MinIO — proxy'ит. Авторизация не требуется (картинки товаров /// публичны как и было раньше); если в будущем понадобится защита — добавить /// [Authorize] и подписать URL. /// /// nginx должен по-прежнему перехватывать /uploads/ и проксировать на /// API (см. deploy/nginx.conf). Сейчас локальный root монтируется в volume и /// nginx раздаёт напрямую — этот контроллер становится альтернативой когда /// MinIO активен. [ApiController] [Route("uploads")] public class UploadsController : ControllerBase { private readonly IObjectStorage _storage; public UploadsController(IObjectStorage storage) => _storage = storage; [HttpGet("{*path}")] public async Task Get(string path, [FromQuery] string? size, CancellationToken ct) { if (string.IsNullOrEmpty(path)) return NotFound(); // Sprint 14: ?size=thumb|medium|original. Запрос с size=thumb // отдаёт .thumb.webp (если существует), иначе fallback // на оригинал. Это позволяет фронту использовать с // srcset для разных ширин экрана. var variantSuffix = size?.ToLowerInvariant() switch { "thumb" => foodmarket.Api.Storage.ImageVariantService.ThumbSuffix, "medium" => foodmarket.Api.Storage.ImageVariantService.MediumSuffix, _ => "", }; if (variantSuffix.Length > 0) { var variantPath = path + variantSuffix; var variant = await _storage.OpenAsync(variantPath, ct); if (variant is not null) { Response.Headers["Cache-Control"] = "public, max-age=2592000"; // 30 дней (агрессивнее для variant'ов) return File(variant.Value.Stream, variant.Value.ContentType); } // Fallback на оригинал — старые загрузки до Sprint 14 не имеют variant'ов. } var obj = await _storage.OpenAsync(path, ct); if (obj is null) return NotFound(); Response.Headers["Cache-Control"] = "public, max-age=604800"; // 7 дней return File(obj.Value.Stream, obj.Value.ContentType); } }