using LiteCqrs; using TeleWave.Api.Common; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Images; using TeleWave.Application.Images.DeleteImage; using TeleWave.Application.Images.GetImageFile; using TeleWave.Application.Images.ListImages; using TeleWave.Application.Images.UploadImage; using TeleWave.Domain.Images; using TeleWave.Infrastructure.Identity; namespace TeleWave.Api.Endpoints; public static class ImageEndpoints { private const long MaxBytes = 50L * 1024 * 1024; // 50 МБ private static readonly IReadOnlySet AllowedExtensions = new HashSet( StringComparer.OrdinalIgnoreCase ) { ".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif", }; public static IEndpointRouteBuilder MapImageEndpoints(this IEndpointRouteBuilder app) { var admin = app.MapGroup("/api/admin/images") .WithTags("Admin.Images") .RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin)); admin.MapGet("", ListImages).Produces>(); admin.MapPost("", UploadImage).Produces(StatusCodes.Status201Created); admin.MapDelete("/{id:guid}", DeleteImage).Produces(StatusCodes.Status204NoContent); // Публичная отдача файла (для у зрителя и в админке). app.MapGet("/api/images/{id:guid}", ServeImage).WithTags("Images"); return app; } private static async Task ListImages( ImageCategory category, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new ListImagesQuery(category), cancellationToken); return result.ToHttpResult(); } private static async Task UploadImage( ImageCategory category, string fileName, HttpRequest request, IImageStore storage, ISender sender, CancellationToken cancellationToken ) { if (request.ContentLength is > MaxBytes or 0 or null) return ImageErrors.InvalidFile.ToProblem(); var ext = Path.GetExtension(fileName).ToLowerInvariant(); if (!AllowedExtensions.Contains(ext)) return ImageErrors.InvalidFile.ToProblem(); var created = await sender.Send( new UploadImageCommand(category, ext, fileName), cancellationToken ); if (!created.IsSuccess) return created.ToHttpResult(); try { await storage.SaveAsync(created.Value, ext, request.Body, cancellationToken); } catch { // Файл не сохранился — не оставляем висячую запись реестра. await sender.Send(new DeleteImageCommand(created.Value), cancellationToken); throw; } return Results.Created( $"/api/images/{created.Value}", new CreatedIdResponse(created.Value) ); } private static async Task DeleteImage( Guid id, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new DeleteImageCommand(id), cancellationToken); return result.ToHttpResult(); } private static async Task ServeImage( Guid id, HttpResponse response, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new GetImageFileQuery(id), cancellationToken); if (!result.IsSuccess) return Results.NotFound(); response.Headers.CacheControl = "public, max-age=86400"; return Results.File(result.Value, ContentTypeFor(Path.GetExtension(result.Value))); } private static string ContentTypeFor(string extension) => extension.ToLowerInvariant() switch { ".png" => "image/png", ".webp" => "image/webp", ".gif" => "image/gif", ".bmp" => "image/bmp", _ => "image/jpeg", }; }