From 4e3302c144834221084b5858a13d3b1ceedbde05 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sat, 28 Mar 2026 20:47:54 +0100 Subject: [PATCH] Rewrite handlers --- Application/Extensions/ResultExtensions.cs | 24 +++ .../Commands/CreatePlayListCommand.cs | 19 +- .../Handlers/CreatePlayListCommandHandler.cs | 157 +++++++------- .../Handlers/DeletePlayListCommandHandler.cs | 38 +--- .../Handlers/UpdatePlayListCommandHandler.cs | 191 ++++++++++-------- .../Tracks/Commands/CreateTrackCommand.cs | 18 +- .../Tracks/Commands/DeleteTrackCommand.cs | 10 + .../Handler/CreateTrackCommandHandler.cs | 174 ++++++++-------- .../Handler/DeleteTrackCommandHandler.cs | 69 +++++++ .../Users/Commands/CreateUserCommand.cs | 12 +- .../Handlers/CreateUserCommandHandler.cs | 13 +- .../Handlers/DeleteUserCommandHandler.cs | 1 - 12 files changed, 441 insertions(+), 285 deletions(-) create mode 100644 Application/Extensions/ResultExtensions.cs create mode 100644 Application/Tracks/Commands/DeleteTrackCommand.cs create mode 100644 Application/Tracks/Handler/DeleteTrackCommandHandler.cs diff --git a/Application/Extensions/ResultExtensions.cs b/Application/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..d4f0ad5 --- /dev/null +++ b/Application/Extensions/ResultExtensions.cs @@ -0,0 +1,24 @@ +using Ardalis.Result; + +namespace Musify.Application.Extensions +{ + public static class ResultExtensions + { + public static Result ToErrorResult(this Result result) + { + var newResult = result.Status switch + { + ResultStatus.Error => Result.Error(new ErrorList(result.Errors)), + ResultStatus.Forbidden => Result.Forbidden(result.Errors.ToArray()), + ResultStatus.Unauthorized => Result.Unauthorized(result.Errors.ToArray()), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors.ToArray()), + ResultStatus.NotFound => Result.NotFound(result.Errors.ToArray()), + ResultStatus.Conflict => Result.Conflict(result.Errors.ToArray()), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported.") + }; + return newResult; + } + } +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayListCommand.cs index 0ceddcc..cda4122 100644 --- a/Application/PlayLists/Commands/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayListCommand.cs @@ -2,6 +2,7 @@ using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; using Musify.Application.PlayLists.Responses; +using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands { @@ -10,5 +11,21 @@ namespace Musify.Application.PlayLists.Commands string Name, string Description, IFileData? Picture) - : IRequest> >; + : IRequest> > + { + public static PlayList ToEntity(CreatePlayListCommand command, string originalKey, string smallKey, string mediumKey, string largeKey) + { + return new PlayList + { + UserId = command.UserId, + Name = command.Name, + NormalizedName = command.Name.Trim().ToUpperInvariant(), + Description = command.Description, + OriginalPictureName = originalKey, + SmallPictureName = smallKey, + MediumPictureName = mediumKey, + LargePictureName = largeKey, + }; + } + } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 1bdd67d..edccb2d 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -6,6 +6,7 @@ using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -19,101 +20,107 @@ namespace Musify.Application.PlayLists.Handlers public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) { var userExists = await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken); - if (!userExists) { logger.LogWarning("User with Id={UserId} does not exist.", request.UserId); return Result.NotFound($"User with Id {request.UserId} does not exist."); } - var playList = new PlayList + var playListEntity = CreatePlayListCommand.ToEntity(request, + playListConfiguration.Routes.PresetOriginalPicture, + playListConfiguration.Routes.PresetSmallPicture, + playListConfiguration.Routes.PresetMediumPicture, + playListConfiguration.Routes.PresetLargePicture); + + if (request.Picture is not null) { - UserId = request.UserId, - Name = request.Name, - NormalizedName = request.Name.Trim().ToUpperInvariant(), - Description = request.Description, - OriginalPictureName = playListConfiguration.Routes.PresetOriginalPicture, - SmallPictureName = playListConfiguration.Routes.PresetSmallPicture, - MediumPictureName = playListConfiguration.Routes.PresetMediumPicture, - LargePictureName = playListConfiguration.Routes.PresetLargePicture, - }; + var uploadResult = await SetPicture(playListEntity, request.Picture, cancellationToken); + if (!uploadResult.IsSuccess) + return uploadResult; - var setPictureResult = request.Picture is null - ? Result.Success() - : await UploadPicture(playList, request.Picture, cancellationToken); - - if (!setPictureResult.IsSuccess) - return setPictureResult; - - await database.PlayLists.AddAsync(playList, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playList.Id, request.UserId); - return Result.Created(PlayListResponse.FromEntity(playList)); - } - - async Task UploadPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) - { - var imageId = Guid.NewGuid(); - var pictureName = imageId + picture.FileType; - var pictureStorageKey = Path.Join(playListConfiguration.Routes.OriginalPicturesPath, - pictureName); - - var uploadResult = await UploadFile(picture, pictureStorageKey, cancellationToken); - - if (!uploadResult.IsSuccess) - { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error={ErrorMessage}", - playList.Id, string.Join("; ", uploadResult.Errors)); - return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); + var publishResult = await PublishUpdateEvent(playListEntity, cancellationToken); + if (!publishResult.IsSuccess) + { + await RollbackPicture(playListEntity, cancellationToken); + return publishResult; + } } - playList.OriginalPictureName = pictureName; - logger.LogInformation("Playlist image uploaded successfully for PlayList with Id={PlayListId}.", playList.Id); - try { - await PublishUpdateEvent(pictureStorageKey, playList, cancellationToken); + await database.PlayLists.AddAsync(playListEntity, cancellationToken); + await database.SaveChangesAsync(cancellationToken); } catch (Exception exception) { - logger.LogError(exception, "Failed to publish resize picture event for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish resize picture event for PlayList with id {playList.Id}."); + await RollbackPicture(playListEntity, cancellationToken); + logger.LogError(exception, "Failed to create PlayList for user with id={UserId}.", request.UserId); + return Result.Error($"Failed to create PlayList for user with id {request.UserId}."); } + + logger.LogInformation("PlayList with id={PlayListId} created successfully for user with id={UserId}.", playListEntity.Id, request.UserId); + return Result.Created(PlayListResponse.FromEntity(playListEntity)); + } + + async Task RollbackPicture(PlayList playList, CancellationToken cancellationToken) + { + try + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, + Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, playList.OriginalPictureName), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to rollback picture upload for PlayList with Id={PlayListId}.", playList.Id); + } + } + + async Task SetPicture(PlayList playList, IFileData picture, CancellationToken cancellationToken) + { + var pictureName = Guid.NewGuid() + picture.FileType; + var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + + var uploadResult = await storageHandler.UploadFileAsync(picture.FileStream, picture.ContentType, storageConfiguration.BucketName, pictureKey, cancellationToken); + if (!uploadResult.IsSuccess) + { + logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error={ErrorMessage}.", + playList.Id, string.Join(", ", uploadResult.Errors)); + return uploadResult.ToErrorResult(); + } + + playList.OriginalPictureName = pictureName; + return Result.Success(); } - async Task> UploadFile(IFileData fileData, string storageKeyName, CancellationToken cancellationToken) + async Task PublishUpdateEvent(PlayList playList, CancellationToken cancellationToken) { - var uploadFileResult = await storageHandler.UploadFileAsync( - fileData.FileStream, - fileData.ContentType, - storageConfiguration.BucketName, - storageKeyName, - cancellationToken); - - return uploadFileResult; - } - - async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), - cancellationToken); + try + { + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + playList.OriginalPictureName, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with Id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with Id {playList.Id}."); + } } } } \ No newline at end of file diff --git a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs index 1151b5e..4f5be7c 100644 --- a/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs @@ -14,8 +14,9 @@ namespace Musify.Application.PlayLists.Handlers { public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) { - var playList = await database.PlayLists.FindAsync(request.PlayListId, cancellationToken); - + var playList = await database.PlayLists.FindAsync( + [request.PlayListId], + cancellationToken); if (playList is null) { logger.LogDebug("PlayList with id={PlayListId} not found", request.PlayListId); @@ -28,33 +29,15 @@ namespace Musify.Application.PlayLists.Handlers return Result.Unauthorized(); } - var keysToRemove = new List(); - if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.SmallPicturesPath, - playList.SmallPictureName)); + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.SmallPicturesPath, + playList.SmallPictureName), cancellationToken); if (playList.MediumPictureName != playListConfiguration.Routes.PresetMediumPicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.MediumPicturesPath, - playList.MediumPictureName)); + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.MediumPicturesPath, + playList.MediumPictureName), cancellationToken); if (playList.LargePictureName != playListConfiguration.Routes.PresetLargePicture) - keysToRemove.Add(Path.Combine(playListConfiguration.Routes.LargePicturesPath, - playList.LargePictureName)); - - foreach (var key in keysToRemove) - { - var publishResult = await TryPublishRemoveFileEventAsync(playList.Id, key, cancellationToken); - - if (!publishResult.IsSuccess) - { - if (publishResult.IsNotFound()) - { - logger.LogWarning("File with key={Key} not found in storage while trying to publish remove file event for PlayList with id={PlayListId}.", key, playList.Id); - continue; - } - - return publishResult; - } - } + await PublishRemoveFileEventAsync(playList.Id, Path.Combine(playListConfiguration.Routes.LargePicturesPath, + playList.LargePictureName), cancellationToken); database.PlayLists.Remove(playList); await database.SaveChangesAsync(cancellationToken); @@ -62,14 +45,13 @@ namespace Musify.Application.PlayLists.Handlers return Result.NoContent(); } - async Task TryPublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) + async Task PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken) { try { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, key), cancellationToken); - return Result.Success(); } catch (Exception exception) diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index 0647ed9..4666966 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -1,11 +1,11 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; +using Musify.Application.Extensions; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Domain.Entities; @@ -19,38 +19,116 @@ namespace Musify.Application.PlayLists.Handlers { public async Task> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken) { - var playList = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken); - - if (playList is null) + var playListEntity = await database.PlayLists.FindAsync( + [request.PlayListId], + cancellationToken); + if (playListEntity is null) { - logger.LogInformation("PlayList with id={PlayListId} not found", request.PlayListId); + logger.LogInformation("PlayList with id={PlayListId} not found.", request.PlayListId); return Result.NotFound(); } - if (playList.UserId != request.UserId) + if (playListEntity.UserId != request.UserId) { - logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}", request.UserId, request.PlayListId); + logger.LogWarning("User with id={UserId} is not the owner of the PlayList with id={PlayListId}.", request.UserId, request.PlayListId); return Result.Unauthorized(); } - if (!string.IsNullOrWhiteSpace(request.NewName)) - { - playList.Name = request.NewName; - playList.NormalizedName = request.NewName.Trim().ToUpperInvariant(); - } + UpdateName(playListEntity, request.NewName); + UpdateDescription(playListEntity, request.NewDescription); + var updatePictureResult = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken); + if (!updatePictureResult.IsSuccess) + return updatePictureResult.ToErrorResult(); - if (!string.IsNullOrWhiteSpace(request.NewDescription)) - playList.Description = request.NewDescription; - - if (request.NewPicture is null) + try { - database.PlayLists.Update(playList); + database.PlayLists.Update(playListEntity); await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("Updated PlayList with id={PlayListId} without picture changes.", playList.Id); - return Result.Success(PlayListResponse.FromEntity(playList)); + await PublishPlayListPictureEvent(playListEntity, updatePictureResult.Value, cancellationToken); + await RemoveOldPictures(playListEntity, cancellationToken); + } + catch (Exception exception) + { + await Rollback(updatePictureResult.Value, cancellationToken); + logger.LogError(exception, "Failed to update PlayList with id={PlayListId}.", playListEntity.Id); + return Result.Error($"Failed to update PlayList with id {playListEntity.Id}."); } + logger.LogInformation("Successfully updated PlayList with id={PlayListId}.", playListEntity.Id); + return Result.Success(PlayListResponse.FromEntity(playListEntity)); + } + + void UpdateName(PlayList playList, string? newName) + { + if (!string.IsNullOrWhiteSpace(newName)) + { + playList.Name = newName; + playList.NormalizedName = newName.Trim().ToUpperInvariant(); + } + } + + void UpdateDescription(PlayList playList, string? newDescription) + { + if (!string.IsNullOrWhiteSpace(newDescription)) + playList.Description = newDescription; + } + + async Task> UpdatePicture(PlayList playList, IFileData? newPicture, CancellationToken cancellationToken) + { + if (newPicture is null) + return Result.Success(); + + var pictureName = Guid.NewGuid() + newPicture.FileType; + var pictureKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); + var uploadResult = await storageHandler.UploadFileAsync( + newPicture.FileStream, + newPicture.ContentType, + storageConfiguration.BucketName, + pictureKey, + cancellationToken); + + if (!uploadResult.IsSuccess) + { + logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error?={ErrorMessage}", + playList.Id, string.Join("; ", uploadResult.Errors)); + return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); + } + playList.OriginalPictureName = pictureName; + return Result.Success(pictureKey); + } + + async Task PublishPlayListPictureEvent(PlayList playList, string pictureKey, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new UpdatePlayListPictureEvent( + playList.Id, + storageConfiguration.BucketName, + pictureKey, + new ImageSize( + playListConfiguration.Routes.SmallPicturesPath, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + playListConfiguration.Routes.MediumPicturesPath, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + playListConfiguration.Routes.LargePicturesPath, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)), + cancellationToken); + return Result.Success(); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish UpdatePlayListPictureEvent for PlayList with id={PlayListId}.", playList.Id); + return Result.Error($"Failed to publish UpdatePlayListPictureEvent for PlayList with id {playList.Id}."); + } + } + + async Task RemoveOldPictures(PlayList playList, CancellationToken cancellationToken) + { if (playList.SmallPictureName != playListConfiguration.Routes.PresetSmallPicture) await PublishRemoveFileEvent(Path.Combine( playListConfiguration.Routes.SmallPicturesPath, @@ -66,34 +144,15 @@ namespace Musify.Application.PlayLists.Handlers playListConfiguration.Routes.LargePicturesPath, playList.LargePictureName), cancellationToken); + } - var newImageId = Guid.NewGuid(); - var pictureName = newImageId + request.NewPicture.FileType; - var newImageStorageKey = Path.Combine(playListConfiguration.Routes.OriginalPicturesPath, pictureName); - - var uploadResult = await UploadFile(request.NewPicture, newImageStorageKey, cancellationToken); - if (!uploadResult.IsSuccess) - { - logger.LogError("Failed to upload picture for PlayList with Id={PlayListId} to storage. Storage handler returned error?={ErrorMessage}", - playList.Id, string.Join("; ", uploadResult.Errors)); - return Result.Error($"Failed to upload picture for PlayList with Id {playList.Id} to storage."); - } - - try - { - await PublishUpdateEvent(newImageStorageKey, playList, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish update picture event for PlayList with id={PlayListId}.", playList.Id); - return Result.Error($"Failed to publish update picture event for PlayList with id {playList.Id}."); - } - - database.PlayLists.Update(playList); - await database.SaveChangesAsync(cancellationToken); - - logger.LogInformation("Updated PlayList with id={PlayListId} and picture key={PictureKey}.", playList.Id, newImageStorageKey); - return Result.Success(PlayListResponse.FromEntity(playList)); + async Task Rollback(string pictureKey, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync( + storageConfiguration.BucketName, + pictureKey, + cancellationToken); + logger.LogInformation("Rolled back uploaded picture with key={PictureKey} from storage.", pictureKey); } async Task PublishRemoveFileEvent(string key, CancellationToken cancellationToken) @@ -102,10 +161,7 @@ namespace Musify.Application.PlayLists.Handlers { await eventBus.PublishAsync(new RemoveFileEvent( storageConfiguration.BucketName, - Path.Combine( - playListConfiguration.Routes.ParentFolders, - key)), cancellationToken); - + key), cancellationToken); return Result.NoContent(); } catch (Exception exception) @@ -114,38 +170,5 @@ namespace Musify.Application.PlayLists.Handlers return Result.Error($"Failed to publish remove file event for key {key}."); } } - - async Task> UploadFile(IFileData picture, string pictureKeyName, CancellationToken cancellationToken) - { - var uploadFileResult = await storageHandler.UploadFileAsync( - picture.FileStream, - picture.ContentType, - storageConfiguration.BucketName, - pictureKeyName, - cancellationToken); - - return uploadFileResult; - } - - async Task PublishUpdateEvent(string originalPictureKey, PlayList playList, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdatePlayListPictureEvent( - playList.Id, - storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - playListConfiguration.Routes.SmallPicturesPath, - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - playListConfiguration.Routes.MediumPicturesPath, - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - playListConfiguration.Routes.LargePicturesPath, - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight)), - cancellationToken); - } } } \ No newline at end of file diff --git a/Application/Tracks/Commands/CreateTrackCommand.cs b/Application/Tracks/Commands/CreateTrackCommand.cs index 2a6c939..fc762cd 100644 --- a/Application/Tracks/Commands/CreateTrackCommand.cs +++ b/Application/Tracks/Commands/CreateTrackCommand.cs @@ -2,6 +2,7 @@ using DispatchR.Abstractions.Send; using Musify.Application.Contracts.Application; using Musify.Application.Tracks.Responses; +using Musify.Domain.Entities; namespace Musify.Application.Tracks.Commands { @@ -10,5 +11,20 @@ namespace Musify.Application.Tracks.Commands string Title, IFileData Picture, IFileData Audio) - : IRequest >>; + : IRequest >> + { + public static Track ToEntity(CreateTrackCommand command, string smallPictureName, string mediumPictureName, string largePictureName) + { + return new Track + { + Title = command.Title, + NormalizedTitle = command.Title.ToUpperInvariant(), + OriginalPictureName = Guid.NewGuid() + command.Picture.FileType, + OriginalAudioName = Guid.NewGuid() + command.Audio.FileType, + SmallPictureName = smallPictureName, + MediumPictureName = mediumPictureName, + LargePictureName = largePictureName + }; + } + } } \ No newline at end of file diff --git a/Application/Tracks/Commands/DeleteTrackCommand.cs b/Application/Tracks/Commands/DeleteTrackCommand.cs new file mode 100644 index 0000000..5351410 --- /dev/null +++ b/Application/Tracks/Commands/DeleteTrackCommand.cs @@ -0,0 +1,10 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; + +namespace Musify.Application.Tracks.Commands +{ + public record DeleteTrackCommand( + Guid UserId, + Guid TrackId) + : IRequest>; +} \ No newline at end of file diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 423ff4d..893f644 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -2,7 +2,6 @@ using DispatchR.Abstractions.Send; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Events; using Musify.Application.Tracks.Commands; @@ -21,111 +20,118 @@ namespace Musify.Application.Tracks.Handler if (userExists is null) { - logger.LogWarning("User with id={UserId} not found", request.UserId); - return Result.NotFound($"User with id {request.UserId} not found"); + logger.LogWarning("User with id={UserId} not found.", request.UserId); + return Result.NotFound($"User with id {request.UserId} not found."); } - var originalPictureFileName = Guid.NewGuid() + request.Picture.FileType; - var originalAudioFileName = Guid.NewGuid() + request.Audio.FileType; - var track = new Track - { - Title = request.Title, - NormalizedTitle = request.Title.Trim().ToUpperInvariant(), - OriginalPictureName = originalPictureFileName, - OriginalAudioName = originalAudioFileName, - SmallPictureName = trackConfiguration.Routes.PresetMediumPicture, - MediumPictureName = trackConfiguration.Routes.PresetMediumPicture, - LargePictureName = trackConfiguration.Routes.PresetLargePicture, - }; + var trackEntity = CreateTrackCommand.ToEntity( + request, + trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.Routes.PresetLargePicture); - var originalPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, - originalPictureFileName); - var pictureUploadResult = await UploadFile(request.Picture, originalPictureKey, cancellationToken); - if (!pictureUploadResult.IsSuccess) - { - logger.LogError("Failed to upload picture for track={TrackTitle}", request.Title); - return Result.Error($"Failed to upload picture for track {request.Title}"); - } + var fullPictureKey = Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, trackEntity.OriginalPictureName); + var fullAudioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, trackEntity.OriginalAudioName); + await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken); - var audioKey = Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, - Guid.NewGuid() + request.Audio.FileType); - var audioUploadResult = await UploadFile(request.Audio, audioKey, cancellationToken); - if (!audioUploadResult.IsSuccess) - { - logger.LogError("Failed to upload audio for track={TrackTitle}", request.Title); - return Result.Error($"Failed to upload audio for track {request.Title}"); - } + await PublishUpdateEvent(trackEntity, cancellationToken); + await PublishTranscodeEvent(trackEntity, cancellationToken); - try - { - await PublishUpdateEvent(originalPictureKey, track, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); - return Result.Error($"Failed to publish resize picture event for Track with id {track.Id}."); - } - - try - { - await PublishTranscodeEvent(track.Id, audioUploadResult, cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); - return Result.Error($"An error occurred while processing the audio for Track with id {track.Id}."); - } - - await database.Tracks.AddAsync(track, cancellationToken); + await database.Tracks.AddAsync(trackEntity, cancellationToken); await database.SaveChangesAsync(cancellationToken); return Result.Success(); } - async Task> UploadFile(IFileData fileData, string key, CancellationToken cancellationToken) + async Task UploadFilesAsync( + CreateTrackCommand request, + string pictureKey, + string audioKey, + CancellationToken cancellationToken) { - var uploadFile = await storageHandler.UploadFileAsync( - fileData.FileStream, - fileData.ContentType, + var pictureUpload = await storageHandler.UploadFileAsync( + request.Picture.FileStream, + request.Picture.ContentType, storageConfiguration.BucketName, - key, + pictureKey, cancellationToken); - return uploadFile; - } + if (!pictureUpload.IsSuccess) + { + var errors = string.Join(", ", pictureUpload.Errors); + return Result.Error($"Failed to upload picture for track {request.Title}. Errors: {errors}."); + } - async Task PublishUpdateEvent(string originalPictureKey, Track track, CancellationToken cancellationToken) - { - await eventBus.PublishAsync(new UpdateTrackPictureEvent( - track.Id, + var audioUpload = await storageHandler.UploadFileAsync( + request.Audio.FileStream, + request.Audio.ContentType, storageConfiguration.BucketName, - originalPictureKey, - new ImageSize( - trackConfiguration.Routes.PresetSmallPicture, - trackConfiguration.PicturesSizes.SmallPictureWidth, - trackConfiguration.PicturesSizes.SmallPictureHeight), - new ImageSize( - trackConfiguration.Routes.PresetMediumPicture, - trackConfiguration.PicturesSizes.MediumPictureWidth, - trackConfiguration.PicturesSizes.MediumPictureHeight), - new ImageSize( - trackConfiguration.Routes.PresetLargePicture, - trackConfiguration.PicturesSizes.LargePictureWidth, - trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); + audioKey, + cancellationToken); + + if (!audioUpload.IsSuccess) + { + await RollbackFilesAsync(pictureKey, audioKey, cancellationToken); + var errors = string.Join(", ", audioUpload.Errors); + return Result.Error($"Failed to upload audio for track {request.Title}. Errors: {errors}."); + } + + return Result.Success(); } - async Task PublishTranscodeEvent(Guid trackId, string audioKey, CancellationToken cancellationToken) + async Task RollbackFilesAsync(string pictureKey, string audioKey, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, pictureKey, cancellationToken); + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, audioKey, cancellationToken); + logger.LogInformation("Rolled back uploaded files={PictureKey}, {AudioKey}.", pictureKey, audioKey); + } + + async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken) + { + try + { + await eventBus.PublishAsync(new UpdateTrackPictureEvent( + track.Id, + storageConfiguration.BucketName, + track.OriginalPictureName, + new ImageSize( + trackConfiguration.Routes.PresetSmallPicture, + trackConfiguration.PicturesSizes.SmallPictureWidth, + trackConfiguration.PicturesSizes.SmallPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetMediumPicture, + trackConfiguration.PicturesSizes.MediumPictureWidth, + trackConfiguration.PicturesSizes.MediumPictureHeight), + new ImageSize( + trackConfiguration.Routes.PresetLargePicture, + trackConfiguration.PicturesSizes.LargePictureWidth, + trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish resize picture event for Track with id={TrackId}.", track.Id); + } + } + + async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken) { var destinationFolderAudio = Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, Guid.NewGuid().ToString()); - await eventBus.PublishAsync(new UpdateTrackAudioEvent( - trackId, - storageConfiguration.BucketName, - audioKey, - storageConfiguration.BucketName, - destinationFolderAudio), cancellationToken); + try + { + await eventBus.PublishAsync(new UpdateTrackAudioEvent( + track.Id, + storageConfiguration.BucketName, + track.OriginalAudioName, + storageConfiguration.BucketName, + destinationFolderAudio), cancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to publish transcode audio event for Track with id={TrackId}.", track.Id); + } } } -} +} \ No newline at end of file diff --git a/Application/Tracks/Handler/DeleteTrackCommandHandler.cs b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs new file mode 100644 index 0000000..6d765ea --- /dev/null +++ b/Application/Tracks/Handler/DeleteTrackCommandHandler.cs @@ -0,0 +1,69 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Tracks.Commands; +using Musify.Domain.Entities; + +namespace Musify.Application.Tracks.Handler +{ + public class DeleteTrackCommandHandler(IDatabase database, IStorageHandler storageHandler, ILogger logger, + TrackConfiguration trackConfiguration, ApplicationStorageConfiguration storageConfiguration) + : IRequestHandler> + { + public async Task Handle(DeleteTrackCommand request, CancellationToken cancellationToken) + { + var track = await database.Tracks.FindAsync(request.TrackId, cancellationToken); + + if (track is null) + { + logger.LogInformation("Track with id={TrackId} not found.", request.TrackId); + return Result.NotFound(); + } + + if (track.Id != request.UserId) + { + logger.LogInformation("User with id={UserId} is not the owner of track with id={TrackId}.", request.UserId, request.TrackId); + return Result.Unauthorized(); + } + + await RemovePictures(track, cancellationToken); + await RemoveAudios(track, cancellationToken); + + return Result.NoContent(); + } + + async Task RemovePictures(Track track, CancellationToken cancellationToken) + { + await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalPicturesPath, track.OriginalPictureName), + cancellationToken); + + if (track.SmallPictureName != trackConfiguration.Routes.PresetSmallPicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.SmallPicturesPath, track.SmallPictureName), + cancellationToken); + if (track.MediumPictureName != trackConfiguration.Routes.PresetMediumPicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.MediumPicturesPath, track.MediumPictureName), + cancellationToken); + if (track.LargePictureName != trackConfiguration.Routes.PresetLargePicture) + await RemoveFile(Path.Combine(trackConfiguration.Routes.LargePicturesPath, track.LargePictureName), + cancellationToken); + + } + + async Task RemoveAudios(Track track, CancellationToken cancellationToken) + { + await RemoveFile(Path.Combine(trackConfiguration.Routes.OriginalAudiosPath, track.OriginalAudioName), + cancellationToken); + + await RemoveFile(Path.Combine(trackConfiguration.Routes.ProcessedAudiosPath, track.AudioFolderName), + cancellationToken); + } + + async Task RemoveFile(string path, CancellationToken cancellationToken) + { + await storageHandler.RemoveFileAsync(storageConfiguration.BucketName, path, cancellationToken); + logger.LogDebug("File with path={Path} removed from storage.", path); + } + } +} \ No newline at end of file diff --git a/Application/Users/Commands/CreateUserCommand.cs b/Application/Users/Commands/CreateUserCommand.cs index afa6f51..fcc77b7 100644 --- a/Application/Users/Commands/CreateUserCommand.cs +++ b/Application/Users/Commands/CreateUserCommand.cs @@ -1,9 +1,19 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; using Musify.Application.Users.Responses; +using Musify.Domain.Entities; namespace Musify.Application.Users.Commands { public record CreateUserCommand(Guid Id) - : IRequest>>; + : IRequest> > + { + public static User ToEntity(CreateUserCommand command) + { + return new User + { + Id = command.Id + }; + } + } } \ No newline at end of file diff --git a/Application/Users/Handlers/CreateUserCommandHandler.cs b/Application/Users/Handlers/CreateUserCommandHandler.cs index a7debf7..585016f 100644 --- a/Application/Users/Handlers/CreateUserCommandHandler.cs +++ b/Application/Users/Handlers/CreateUserCommandHandler.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; using Musify.Application.Users.Commands; using Musify.Application.Users.Responses; -using Musify.Domain.Entities; namespace Musify.Application.Users.Handlers { @@ -14,27 +13,21 @@ namespace Musify.Application.Users.Handlers { public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken) { - var existingUser = await database.Users.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken); - - if (existingUser is not null) + var userExists = await database.Users.AnyAsync(u => u.Id == request.Id, cancellationToken); + if (!userExists) { logger.LogInformation("User with Id={UserId} already exists.", request.Id); return Result.Conflict($"A user with Id {request.Id} already exists."); } var keycloakUser = await keycloakUserClient.GetUserByIdAsync(request.Id.ToString(), cancellationToken); - if (!keycloakUser.IsSuccess) { logger.LogWarning("Keycloak user with Id={UserId} was not found.", request.Id); return Result.NotFound("Keycloak user not found."); } - var newUser = new User - { - Id = request.Id - }; - + var newUser = CreateUserCommand.ToEntity(request); await database.Users.AddAsync(newUser, cancellationToken); await database.SaveChangesAsync(cancellationToken); diff --git a/Application/Users/Handlers/DeleteUserCommandHandler.cs b/Application/Users/Handlers/DeleteUserCommandHandler.cs index 906bee3..dbab4e7 100644 --- a/Application/Users/Handlers/DeleteUserCommandHandler.cs +++ b/Application/Users/Handlers/DeleteUserCommandHandler.cs @@ -15,7 +15,6 @@ namespace Musify.Application.Users.Commands return Result.NotFound(); database.Users.Remove(existingUser); - await database.SaveChangesAsync(cancellationToken); return Result.NoContent();