132 lines
4.3 KiB
C#
132 lines
4.3 KiB
C#
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 МБ
|
||
|
||
// Тип поля — HashSet, а не IReadOnlySet: поле приватное, и через интерфейс Contains уходит
|
||
// в виртуальный вызов вместо прямого.
|
||
private static readonly HashSet<string> AllowedExtensions = new(
|
||
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<IReadOnlyList<ImageDto>>();
|
||
admin.MapPost("", UploadImage).Produces<CreatedIdResponse>(StatusCodes.Status201Created);
|
||
admin.MapDelete("/{id:guid}", DeleteImage).Produces(StatusCodes.Status204NoContent);
|
||
|
||
// Публичная отдача файла (для <img> у зрителя и в админке).
|
||
app.MapGet("/api/images/{id:guid}", ServeImage).WithTags("Images");
|
||
|
||
return app;
|
||
}
|
||
|
||
private static async Task<IResult> ListImages(
|
||
ImageCategory category,
|
||
ISender sender,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
var result = await sender.Send(new ListImagesQuery(category), cancellationToken);
|
||
return result.ToHttpResult();
|
||
}
|
||
|
||
private static async Task<IResult> 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<IResult> DeleteImage(
|
||
Guid id,
|
||
ISender sender,
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
var result = await sender.Send(new DeleteImageCommand(id), cancellationToken);
|
||
return result.ToHttpResult();
|
||
}
|
||
|
||
private static async Task<IResult> 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",
|
||
};
|
||
}
|