Generated response

This commit is contained in:
2026-05-14 19:53:26 +02:00
parent 7aef20fe71
commit deb4bfda90
33 changed files with 1555 additions and 102 deletions
@@ -1,4 +1,4 @@
using Ardalis.Result;
using Ardalis.Result;
using Mediator;
using Musify.Application.PlayLists.Responses;
@@ -8,6 +8,6 @@ namespace Musify.Application.PlayLists.Commands
Guid UserId,
string Name,
string Description,
string? OriginalPictureName)
Guid? PictureIntentId)
: ICommand<Result<PlayListApplicationResponse>>;
}
}
@@ -7,6 +7,7 @@ namespace Musify.Application.PlayLists.Commands
public record RequestPlayListPictureUploadCommand(
Guid UserId,
string FileType,
string ContentType)
string ContentType,
long? ExpectedSizeBytes = null)
: ICommand<Result<PlayListPictureUploadResponse>>;
}
@@ -1,4 +1,4 @@
using Ardalis.Result;
using Ardalis.Result;
using Mediator;
using Musify.Application.PlayLists.Responses;
@@ -9,6 +9,6 @@ namespace Musify.Application.PlayLists.Commands
Guid PlayListId,
string? NewName,
string? NewDescription,
string? NewOriginalPictureName)
Guid? NewPictureIntentId)
: ICommand<Result<PlayListApplicationResponse>>;
}
}
@@ -7,6 +7,7 @@ using Musify.Application.Abstractions.Infrastructure;
using Musify.Application.Events;
using Musify.Application.PlayLists.Commands;
using Musify.Application.PlayLists.Responses;
using Musify.Application.UploadIntents;
using Musify.Domain.Entities;
namespace Musify.Application.PlayLists.Handlers
@@ -14,9 +15,11 @@ namespace Musify.Application.PlayLists.Handlers
public class CreatePlayListCommandHandler(
IEventBus eventBus,
IDatabase database,
IStorageService storageService,
ILogger<CreatePlayListCommandHandler> logger,
ApplicationStorageConfiguration storageConfiguration,
PlayListConfiguration playListConfiguration)
PlayListConfiguration playListConfiguration,
UploadIntentConfiguration uploadIntentConfiguration)
: ICommandHandler<CreatePlayListCommand, Result<PlayListApplicationResponse>>
{
public async ValueTask<Result<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
@@ -30,7 +33,35 @@ namespace Musify.Application.PlayLists.Handlers
return Result.NotFound($"User {request.UserId} not found");
}
var originalPictureName = request.OriginalPictureName ?? playListConfiguration.Routes.PresetOriginalPicture;
string originalPictureName;
UploadIntent? pictureIntent = null;
if (request.PictureIntentId.HasValue)
{
var validation = await UploadIntentHelpers.ValidateAndLoadAsync(
database, storageService, uploadIntentConfiguration,
request.PictureIntentId.Value, request.UserId, cancellationToken);
if (!validation.IsSuccess)
return validation.Error!.Value;
pictureIntent = validation.Intent!;
originalPictureName = pictureIntent.ObjectName;
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName);
try
{
await storageService.CopyFileAsync(pictureIntent.Bucket, pictureIntent.Key, pictureIntent.Bucket, finalKey, cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", pictureIntent.Key, finalKey);
return Result.Error("Failed to move uploaded file to its final location.");
}
}
else
{
originalPictureName = playListConfiguration.Routes.PresetOriginalPicture;
}
var playList = new PlayList
{
@@ -46,8 +77,10 @@ namespace Musify.Application.PlayLists.Handlers
await database.PlayLists.AddAsync(playList, cancellationToken);
if (request.OriginalPictureName is not null)
if (pictureIntent is not null)
{
pictureIntent.Status = UploadIntentStatus.Consumed;
var publishResult = await PublishPictureUpdateEventAsync(playList, cancellationToken);
if (!publishResult.IsSuccess)
return publishResult;
@@ -98,4 +131,4 @@ namespace Musify.Application.PlayLists.Handlers
}
}
}
}
}
@@ -6,6 +6,8 @@ using Musify.Application.Abstractions.Infrastructure;
using Musify.Application.Configuration;
using Musify.Application.PlayLists.Commands;
using Musify.Application.PlayLists.Responses;
using Musify.Application.UploadIntents;
using Musify.Domain.Entities;
namespace Musify.Application.PlayLists.Handlers
{
@@ -14,11 +16,11 @@ namespace Musify.Application.PlayLists.Handlers
IStorageService storageService,
ILogger<RequestPlayListPictureUploadCommandHandler> logger,
ApplicationStorageConfiguration storageConfiguration,
PlayListConfiguration playListConfiguration)
PlayListConfiguration playListConfiguration,
UploadIntentConfiguration uploadIntentConfiguration)
: ICommandHandler<RequestPlayListPictureUploadCommand, Result<PlayListPictureUploadResponse>>
{
static readonly string[] AllowedExtensions = [".webp", ".png", ".jpg", ".jpeg"];
const int ExpiresInSeconds = 600;
public async ValueTask<Result<PlayListPictureUploadResponse>> Handle(RequestPlayListPictureUploadCommand request, CancellationToken cancellationToken)
{
@@ -31,31 +33,61 @@ namespace Musify.Application.PlayLists.Handlers
return Result.NotFound($"User {request.UserId} not found");
}
var extensionResult = ValidateExtension(request.FileType);
var extensionResult = UploadIntentHelpers.ValidateExtension(request.FileType, AllowedExtensions);
if (!extensionResult.IsSuccess)
return Result.Invalid(extensionResult.Errors.Select(error => new ValidationError(error)).ToArray());
if (string.IsNullOrWhiteSpace(request.ContentType))
return Result.Invalid(new ValidationError("ContentType is required"));
var effectiveSizeBytes = request.ExpectedSizeBytes
?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
var pictureName = Guid.NewGuid() + extensionResult.Value;
var key = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureName);
var objectName = Guid.NewGuid() + extensionResult.Value;
var tempKey = playListConfiguration.Routes.BuildTempPicturePath(
uploadIntentConfiguration.TempRootPrefix, request.UserId, objectName);
try
{
var uploadUrl = await storageService.GetUploadUrlAsync(
storageConfiguration.Bucket,
key,
tempKey,
request.ContentType,
TimeSpan.FromSeconds(ExpiresInSeconds),
TimeSpan.FromSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds),
cancellationToken);
await using var transaction = await database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable, cancellationToken);
var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync(
database, uploadIntentConfiguration, logger,
request.UserId, effectiveSizeBytes, 1, cancellationToken);
if (!quotaCheck.IsSuccess)
return quotaCheck;
var intent = new UploadIntent
{
UserId = request.UserId,
Bucket = storageConfiguration.Bucket,
Key = tempKey,
ObjectName = objectName,
ContentType = request.ContentType,
ExpectedSizeBytes = effectiveSizeBytes,
Purpose = UploadIntentPurpose.PlayListPicture,
ExpiresAt = DateTime.UtcNow.AddSeconds(uploadIntentConfiguration.UploadUrlExpiresInSeconds),
};
await database.UploadIntents.AddAsync(intent, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (PlayListPicture, temp key: {Key})", intent.Id, request.UserId, tempKey);
return Result.Success(new PlayListPictureUploadResponse(
intent.Id,
storageConfiguration.Bucket,
key,
pictureName,
tempKey,
objectName,
request.ContentType,
ExpiresInSeconds,
uploadIntentConfiguration.UploadUrlExpiresInSeconds,
uploadUrl));
}
catch (Exception exception)
@@ -64,25 +96,5 @@ namespace Musify.Application.PlayLists.Handlers
return Result.Error("Failed to generate upload URL");
}
}
static Result<string> ValidateExtension(string fileType)
{
if (string.IsNullOrWhiteSpace(fileType))
return Result.Error("FileType is required");
var extension = fileType.Trim();
if (!extension.StartsWith('.'))
extension = "." + extension;
extension = extension.ToLowerInvariant();
if (extension.Contains('/') || extension.Contains('\\'))
return Result.Error("Invalid FileType");
if (!AllowedExtensions.Contains(extension))
return Result.Error($"Unsupported FileType '{extension}'");
return Result.Success(extension);
}
}
}
@@ -7,15 +7,19 @@ using Musify.Application.Abstractions.Infrastructure;
using Musify.Application.Events;
using Musify.Application.PlayLists.Commands;
using Musify.Application.PlayLists.Responses;
using Musify.Application.UploadIntents;
using Musify.Domain.Entities;
namespace Musify.Application.PlayLists.Handlers
{
public class UpdatePlayListCommandHandler(
IEventBus eventBus,
IDatabase database,
IStorageService storageService,
ILogger<UpdatePlayListCommandHandler> logger,
ApplicationStorageConfiguration storageConfiguration,
PlayListConfiguration playListConfiguration)
PlayListConfiguration playListConfiguration,
UploadIntentConfiguration uploadIntentConfiguration)
: ICommandHandler<UpdatePlayListCommand, Result<PlayListApplicationResponse>>
{
public async ValueTask<Result<PlayListApplicationResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
@@ -44,9 +48,28 @@ namespace Musify.Application.PlayLists.Handlers
playListEntity.Description = request.NewDescription;
}
if (request.NewOriginalPictureName is not null)
if (request.NewPictureIntentId.HasValue)
{
playListEntity.OriginalPictureName = request.NewOriginalPictureName;
var validation = await UploadIntentHelpers.ValidateAndLoadAsync(
database, storageService, uploadIntentConfiguration,
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
if (!validation.IsSuccess)
return validation.Error!.Value;
var intent = validation.Intent!;
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName);
try
{
await storageService.CopyFileAsync(intent.Bucket, intent.Key, intent.Bucket, finalKey, cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to copy playlist picture from temp {TempKey} to final {FinalKey}", intent.Key, finalKey);
return Result.Error("Failed to move uploaded file to its final location.");
}
playListEntity.OriginalPictureName = intent.ObjectName;
intent.Status = UploadIntentStatus.Consumed;
var publishResult = await PublishPlayListPictureEvent(playListEntity.UserId, playListEntity.Id, playListEntity.OriginalPictureName, cancellationToken);
if (!publishResult.IsSuccess)
@@ -1,6 +1,7 @@
namespace Musify.Application.PlayLists.Responses
{
public record PlayListPictureUploadResponse(
Guid IntentId,
string Bucket,
string Key,
string PictureName,