130 lines
4.1 KiB
C#
130 lines
4.1 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 МБ
|
|
|
|
private static readonly IReadOnlySet<string> AllowedExtensions = new HashSet<string>(
|
|
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",
|
|
};
|
|
}
|