Implement media storage and processing features: update configuration in .env.example and appsettings files, enhance Docker setup for media storage, and add media-related services and database entities. Include ffmpeg for media handling and adjust Kestrel settings for large file uploads.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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.Media;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
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<UploadMediaResponse>(StatusCodes.Status201Created);
|
||||
admin.MapGet("", List).Produces<PagedList<MediaAssetDto>>();
|
||||
admin.MapGet("/{id:guid}", Get).Produces<MediaAssetDto>();
|
||||
admin.MapDelete("/{id:guid}", Delete).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Потоковая загрузка: тело запроса — сырые байты файла, имя передаётся в query «fileName».
|
||||
/// Файл стримится на диск без буферизации в память, затем регистрируется и уходит в обработку.
|
||||
/// </summary>
|
||||
private static async Task<IResult> Upload(
|
||||
string fileName,
|
||||
HttpRequest request,
|
||||
IMediaStorage storage,
|
||||
IMediaProcessingQueue queue,
|
||||
ISender sender,
|
||||
IOptions<MediaOptions> mediaOptions,
|
||||
IOptions<StorageOptions> 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<IResult> 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<IResult> Get(
|
||||
Guid id,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new GetMediaAssetQuery(id), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> 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);
|
||||
Reference in New Issue
Block a user