using LiteCqrs; using Microsoft.Extensions.Options; using TeleWave.Api.Common; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Common.Models; using TeleWave.Application.Media; using TeleWave.Application.Media.Delete; using TeleWave.Application.Media.GetMedia; using TeleWave.Application.Media.ListMedia; using TeleWave.Application.Media.Register; using TeleWave.Domain.Media; using TeleWave.Infrastructure.Identity; using TeleWave.Infrastructure.Media; namespace TeleWave.Api.Endpoints; public static class MediaEndpoints { public static IEndpointRouteBuilder MapMediaEndpoints(this IEndpointRouteBuilder app) { var admin = app.MapGroup("/api/admin/media") .WithTags("Admin.Media") .RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin)); admin.MapPost("", Upload).Produces(StatusCodes.Status201Created); admin.MapGet("", List).Produces>(); admin.MapGet("/{id:guid}", Get).Produces(); admin.MapDelete("/{id:guid}", Delete).Produces(StatusCodes.Status204NoContent); return app; } /// /// Потоковая загрузка: тело запроса — сырые байты файла, имя передаётся в query «fileName». /// Файл стримится на диск без буферизации в память, затем регистрируется и уходит в обработку. /// private static async Task Upload( string fileName, HttpRequest request, IMediaStorage storage, IMediaProcessingQueue queue, ISender sender, IOptions mediaOptions, IOptions storageOptions, CancellationToken cancellationToken ) { if (string.IsNullOrWhiteSpace(fileName)) return Results.Problem( title: MediaErrors.EmptyFileName.Code, detail: MediaErrors.EmptyFileName.Message, statusCode: StatusCodes.Status400BadRequest ); if (!MediaFormats.IsAllowed(fileName)) return Results.Problem( title: MediaErrors.UnsupportedFormat.Code, detail: MediaErrors.UnsupportedFormat.Message, statusCode: StatusCodes.Status400BadRequest ); var contentLength = request.ContentLength ?? 0; if (contentLength > mediaOptions.Value.MaxUploadBytes) return Results.Problem( title: MediaErrors.FileTooLarge.Code, detail: MediaErrors.FileTooLarge.Message, statusCode: StatusCodes.Status400BadRequest ); var free = storage.GetAvailableFreeSpaceBytes(); if (free - contentLength < storageOptions.Value.MinFreeSpaceBytes) return Results.Problem( title: MediaErrors.InsufficientStorage.Code, detail: MediaErrors.InsufficientStorage.Message, statusCode: StatusCodes.Status409Conflict ); var extension = Path.GetExtension(fileName); var token = await storage.SaveUploadAsync(request.Body, extension, cancellationToken); var result = await sender.Send( new RegisterMediaAssetCommand(token, MediaSource.Upload, fileName), cancellationToken ); if (!result.IsSuccess) { storage.DeleteUpload(token); return result.ToHttpResult(); } queue.Enqueue(result.Value); return Results.Created( $"/api/admin/media/{result.Value}", new UploadMediaResponse(result.Value) ); } private static async Task List( int page, int pageSize, MediaAssetStatus[]? status, string? search, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send( new ListMediaAssetsQuery( page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, status ?? [], search ), cancellationToken ); return Results.Ok(result); } private static async Task Get( Guid id, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new GetMediaAssetQuery(id), cancellationToken); return result.ToHttpResult(); } private static async Task Delete( Guid id, ISender sender, CancellationToken cancellationToken ) { var result = await sender.Send(new DeleteMediaAssetCommand(id), cancellationToken); return result.ToHttpResult(); } } public sealed record UploadMediaResponse(Guid Id);