From 9756672160f0bba7bc22fa0e1263a94cb097a2a7 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 20:09:36 +0100 Subject: [PATCH] Changes --- .../Infrastructure/IPictureHandler.cs | 4 + .../Infrastructure/IStorageHandler.cs | 3 + Application/Events/ResizePictureArguments.cs | 7 - Application/Events/ResizePictureEvent.cs | 10 - .../Events/UpdatePlayListPictureEvent.cs | 6 + .../CreatePlayListCommandHandler.cs | 19 +- .../UpdatePlayListCommandHandler.cs | 20 +- .../ConfigurationDependencyInjection.cs | 3 +- .../StorageClientConfiguration.cs | 2 + .../WorkerStorageConfiguration.cs | 21 ++ .../Activities/Arguments/CopyFileArguments.cs | 8 + .../Arguments/RemoveFileArguments.cs | 6 + .../Arguments/ResizePictureArgument.cs | 10 + .../UpdatePlayListPictureArguments.cs | 8 + .../Messaging/Activities/CopyFilesActivity.cs | 39 ++++ .../Activities/RemoveFilesActivity.cs | 30 +++ .../Activities/ResizePictureActivity.cs | 51 +++++ .../UpdatePlayListPictureActivity.cs | 29 +++ .../Messaging/Consumers/RemoveFileConsumer.cs | 123 ----------- .../Consumers/ResizePictureConsumer.cs | 206 ------------------ .../UpdatePlayListPictureConsumer.cs | 82 +++++++ .../MassTransitDependencyInjection.cs | 11 +- Infrastructure/Pictures/PictureHandler.cs | 4 + Infrastructure/Storage/StorageHandler.cs | 26 +++ 24 files changed, 336 insertions(+), 392 deletions(-) delete mode 100644 Application/Events/ResizePictureArguments.cs delete mode 100644 Application/Events/ResizePictureEvent.cs create mode 100644 Application/Events/UpdatePlayListPictureEvent.cs create mode 100644 Infrastructure/Configuration/WorkerStorageConfiguration.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs create mode 100644 Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs create mode 100644 Infrastructure/Messaging/Activities/CopyFilesActivity.cs create mode 100644 Infrastructure/Messaging/Activities/RemoveFilesActivity.cs create mode 100644 Infrastructure/Messaging/Activities/ResizePictureActivity.cs create mode 100644 Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs delete mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs delete mode 100644 Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs diff --git a/Application/Contracts/Infrastructure/IPictureHandler.cs b/Application/Contracts/Infrastructure/IPictureHandler.cs index 63ba2e1..e582b42 100644 --- a/Application/Contracts/Infrastructure/IPictureHandler.cs +++ b/Application/Contracts/Infrastructure/IPictureHandler.cs @@ -4,6 +4,10 @@ namespace Musify.Application.Contracts.Infrastructure { public interface IPictureHandler { + public string FileExtension { get; } + + public string ContentType { get; } + Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IStorageHandler.cs b/Application/Contracts/Infrastructure/IStorageHandler.cs index 613b524..c247c88 100644 --- a/Application/Contracts/Infrastructure/IStorageHandler.cs +++ b/Application/Contracts/Infrastructure/IStorageHandler.cs @@ -13,5 +13,8 @@ namespace Musify.Application.Contracts.Infrastructure Task TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken); Task RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken); + + Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/ResizePictureArguments.cs b/Application/Events/ResizePictureArguments.cs deleted file mode 100644 index 2944d5b..0000000 --- a/Application/Events/ResizePictureArguments.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Musify.Application.Events -{ - public record ResizePictureArguments( - int Width, - int Height, - string SaveOnRoute); -} \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs deleted file mode 100644 index 985bc41..0000000 --- a/Application/Events/ResizePictureEvent.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Musify.Application.Contracts.Application; - -namespace Musify.Application.Events -{ - public record ResizePictureEvent( - Guid JobId, - string BucketName, - string KeyName, - ResizePictureArguments[] Arguments) : IEvent; -} \ No newline at end of file diff --git a/Application/Events/UpdatePlayListPictureEvent.cs b/Application/Events/UpdatePlayListPictureEvent.cs new file mode 100644 index 0000000..258c790 --- /dev/null +++ b/Application/Events/UpdatePlayListPictureEvent.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Events +{ + public record UpdatePlayListPictureEvent( + Guid PlayListId, + string OriginalPictureKeyName); +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 0e0115c..ced2c65 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -60,24 +60,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList try { - await eventBus.PublishAsync(new ResizePictureEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - originalImageStorageKey, - [ - new ResizePictureArguments( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index dc8d1e3..b5dba46 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -138,25 +138,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList try { - await eventBus.PublishAsync(new ResizePictureEvent( - Guid.NewGuid(), - storageConfiguration.BucketName, - newImageStorageKey, - [ - new ResizePictureArguments( - playListConfiguration.PicturesSizes.SmallPictureWidth, - playListConfiguration.PicturesSizes.SmallPictureHeight, - playListConfiguration.Routes.SmallPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.MediumPictureWidth, - playListConfiguration.PicturesSizes.MediumPictureHeight, - playListConfiguration.Routes.MediumPictures), - new ResizePictureArguments( - playListConfiguration.PicturesSizes.LargePictureWidth, - playListConfiguration.PicturesSizes.LargePictureHeight, - playListConfiguration.Routes.LargePictures) - ] - ), cancellationToken); + await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken); } catch (Exception exception) { diff --git a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs index a357138..50f54bf 100644 --- a/Infrastructure/Configuration/ConfigurationDependencyInjection.cs +++ b/Infrastructure/Configuration/ConfigurationDependencyInjection.cs @@ -11,7 +11,8 @@ namespace Musify.Infrastructure.Configuration serviceDescriptors .AddValidatedConfiguration(configuration, DatabaseConfiguration.SectionName) .AddValidatedConfiguration(configuration, KeycloakConfiguration.SectionName) - .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName); + .AddValidatedConfiguration(configuration, StorageClientConfiguration.SectionName) + .AddValidatedConfiguration(configuration, WorkerStorageConfiguration.SectionName); return serviceDescriptors; } diff --git a/Infrastructure/Configuration/StorageClientConfiguration.cs b/Infrastructure/Configuration/StorageClientConfiguration.cs index 7e7fc62..0175c0a 100644 --- a/Infrastructure/Configuration/StorageClientConfiguration.cs +++ b/Infrastructure/Configuration/StorageClientConfiguration.cs @@ -18,8 +18,10 @@ namespace Musify.Infrastructure.Configuration [MinLength(8)] public required string SecretAccessKey { get; set; } + [Required] public bool ForcePathStyle { get; set; } + [Required] public bool UseHttp { get; set; } } } diff --git a/Infrastructure/Configuration/WorkerStorageConfiguration.cs b/Infrastructure/Configuration/WorkerStorageConfiguration.cs new file mode 100644 index 0000000..6b556e1 --- /dev/null +++ b/Infrastructure/Configuration/WorkerStorageConfiguration.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Musify.Infrastructure.Configuration +{ + public class WorkerStorageConfiguration + { + public const string SectionName = "WorkerStorage"; + + public Routes Routes { get; set; } = new Routes(); + + [Required] + [MinLength(3)] + public required string BucketName { get; set; } + } + + public class Routes + { + [Required] + public string TemporaryFiles { get; set; } = "TemporaryFiles"; + } +} diff --git a/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs new file mode 100644 index 0000000..b5bbc96 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/CopyFileArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record CopyFileArguments( + string SourceBucketName, + string SourceKeyName, + string DestinationBucketName, + string DestinationKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs b/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs new file mode 100644 index 0000000..a0e8851 --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/RemoveFileArguments.cs @@ -0,0 +1,6 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record RemoveFileArguments( + string BucketName, + string KeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs b/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs new file mode 100644 index 0000000..8d54d7e --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/ResizePictureArgument.cs @@ -0,0 +1,10 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record ResizePictureArgument( + string OriginalBucketName, + string OriginalKeyName, + string DestinationBucketName, + string DestinationKeyName, + int Width, + int Height); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs b/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs new file mode 100644 index 0000000..7fbd4bc --- /dev/null +++ b/Infrastructure/Messaging/Activities/Arguments/UpdatePlayListPictureArguments.cs @@ -0,0 +1,8 @@ +namespace Musify.Infrastructure.Messaging.Activities.Arguments +{ + public record UpdatePlayListPictureArguments( + Guid PlayListId, + string SmallPictureKeyName, + string MediumPictureKeyName, + string LargePictureKeyName); +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/CopyFilesActivity.cs b/Infrastructure/Messaging/Activities/CopyFilesActivity.cs new file mode 100644 index 0000000..64499c2 --- /dev/null +++ b/Infrastructure/Messaging/Activities/CopyFilesActivity.cs @@ -0,0 +1,39 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class CopyFilesActivity(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + foreach (var file in executeContext.Arguments) + { + var copyFileResult = await storageHandler.CopyFileAsync( + file.SourceBucketName, + file.SourceKeyName, + file.DestinationBucketName, + file.SourceKeyName, + executeContext.CancellationToken); + + if (!copyFileResult.IsSuccess) + { + string errorMessage = string.Join("; ", copyFileResult.Errors); + logger.LogError( + "Failed to copy file from {SourceBucket}/{SourceKey} to {DestinationBucket}/{DestinationKey}. Error: {ErrorMessage}", + file.SourceBucketName, + file.SourceKeyName, + file.DestinationBucketName, + file.DestinationKeyName, + errorMessage); + throw new Exception(errorMessage); + } + } + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs new file mode 100644 index 0000000..ef53c37 --- /dev/null +++ b/Infrastructure/Messaging/Activities/RemoveFilesActivity.cs @@ -0,0 +1,30 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class RemoveFilesActivity(IStorageHandler storageHandler, ILogger logger) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + foreach (var file in executeContext.Arguments) + { + var removeFile = await storageHandler.RemoveFileAsync( + file.BucketName, + file.KeyName, + executeContext.CancellationToken); + + if (!removeFile.IsSuccess) + { + string errorMessage = string.Join(";", removeFile.Errors); + logger.LogWarning("Failed to remove file {File} from bucket {BucketName}: {ErrorMessage}", + file, file.BucketName, errorMessage); + } + } + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/ResizePictureActivity.cs b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs new file mode 100644 index 0000000..868a02b --- /dev/null +++ b/Infrastructure/Messaging/Activities/ResizePictureActivity.cs @@ -0,0 +1,51 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class ResizePictureActivity(IStorageHandler storageHandler, IPictureHandler pictureHandler) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + var file = await storageHandler.GetFileAsync( + executeContext.Arguments.OriginalBucketName, + executeContext.Arguments.OriginalKeyName, + executeContext.CancellationToken); + + if (!file.IsSuccess) + { + string errorMessages = string.Join(", ", file.Errors); + throw new Exception(errorMessages); + } + + var resizedPicture = await pictureHandler.ResizePictureAsync( + file.Value, + executeContext.Arguments.Width, + executeContext.Arguments.Height, + executeContext.CancellationToken); + + if (!resizedPicture.IsSuccess) + { + string errorMessage = string.Join(", ", resizedPicture.Errors); + throw new Exception(errorMessage); + } + + var uploadFile = await storageHandler.UploadFileAsync( + resizedPicture.Value, + pictureHandler.ContentType, + executeContext.Arguments.DestinationBucketName, + executeContext.Arguments.DestinationKeyName, + executeContext.CancellationToken); + + if (!uploadFile.IsSuccess) + { + var errorMessage = string.Join(", ", uploadFile.Errors); + throw new Exception(errorMessage); + } + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs new file mode 100644 index 0000000..2266ba1 --- /dev/null +++ b/Infrastructure/Messaging/Activities/UpdatePlayListPictureActivity.cs @@ -0,0 +1,29 @@ +using MassTransit; +using Musify.Application.Contracts.Infrastructure; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Activities +{ + public class UpdatePlayListPictureActivity(IDatabase database) + : IExecuteActivity + { + public async Task Execute(ExecuteContext executeContext) + { + var PlayList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId); + + if (PlayList is null) + { + throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); + } + + PlayList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; + PlayList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; + PlayList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; + + database.PlayLists.Update(PlayList); + await database.SaveChangesAsync(executeContext.CancellationToken); + + return executeContext.Completed(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs deleted file mode 100644 index ed33dbb..0000000 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ /dev/null @@ -1,123 +0,0 @@ -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Events; -using Musify.Domain.Entities; -using System.Text.Json; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class RemoveFileConsumer(IDatabase database, IStorageHandler storageHandler, ILogger logger) - : IConsumer - { - public const string QueueName = "file-remove-queue"; - - public async Task Consume(ConsumeContext consumeContext) - { - var message = consumeContext.Message; - var cancellationToken = consumeContext.CancellationToken; - - var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobOperation = await CreateJobOperationAsync(job.Id, cancellationToken); - await database.SaveChangesAsync(cancellationToken); - - try - { - jobOperation.JobState = JobState.InProgress; - await database.SaveChangesAsync(cancellationToken); - - var removeResult = await storageHandler.RemoveFileAsync( - message.BucketName, - message.KeyName, - cancellationToken); - - if (!removeResult.IsSuccess) - { - MarkAsFailed(jobOperation, string.Join("; ", removeResult.Errors)); - - logger.LogError( - "Failed to remove file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}. Error: {ErrorMessage}", - message.BucketName, - message.KeyName, - job.Id, - jobOperation.Id, - jobOperation.ErrorMessage); - - await database.SaveChangesAsync(cancellationToken); - return; - } - - jobOperation.JobState = JobState.Completed; - jobOperation.FinishedAt = DateTime.UtcNow; - - logger.LogInformation( - "Successfully removed file {BucketName}/{KeyName} for JobId={JobId}, JobOperationId={JobOperationId}.", - message.BucketName, - message.KeyName, - job.Id, - jobOperation.Id); - - await database.SaveChangesAsync(cancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, - "An error occurred while consuming RemoveFileConsumer message for JobId={JobId}, BucketName={BucketName}, KeyName={KeyName}", - message.JobId, - message.BucketName, - message.KeyName); - MarkAsFailed(jobOperation, exception.Message); - await database.SaveChangesAsync(cancellationToken); - - throw; - } - } - - async Task GetOrCreateJobAsync(RemoveFileEvent message, CancellationToken cancellationToken) - { - var job = await database.Jobs - .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); - - if (job is not null) - { - return job; - } - - job = new Job - { - Id = message.JobId, - JobType = JobType.RemoveFile, - Payload = JsonSerializer.Serialize(message) - }; - - await database.Jobs.AddAsync(job, cancellationToken); - - logger.LogInformation( - "Created new job with id {JobId} for removing file {BucketName}/{KeyName}", - job.Id, - message.BucketName, - message.KeyName); - - return job; - } - - async Task CreateJobOperationAsync(Guid jobId, CancellationToken cancellationToken) - { - var jobOperation = new JobOperation - { - JobId = jobId - }; - - await database.JobOperations.AddAsync(jobOperation, cancellationToken); - return jobOperation; - } - - void MarkAsFailed(JobOperation jobOperation, string errorMessage) - { - jobOperation.JobState = JobState.Failed; - jobOperation.ErrorMessage = errorMessage; - jobOperation.FinishedAt = DateTime.UtcNow; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs deleted file mode 100644 index ca8cb45..0000000 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ /dev/null @@ -1,206 +0,0 @@ -using Ardalis.Result; -using MassTransit; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Events; -using Musify.Domain.Entities; -using System.Text.Json; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class ResizePictureConsumer(IStorageHandler storageHandler, IPictureHandler pictureHandler, IDatabase database, ILogger logger) - : IConsumer - { - private record JobOperationItem(ResizePictureArguments Item, JobOperation JobOperation); - - public const string QueueName = "picture-resize-queue"; - - public async Task Consume(ConsumeContext consumeContext) - { - try - { - var message = consumeContext.Message; - var cancellationToken = consumeContext.CancellationToken; - - var job = await GetOrCreateJobAsync(message, cancellationToken); - var jobOperations = await CreateJobOperationAsync(message.Arguments, job.Id, cancellationToken); - - await database.SaveChangesAsync(cancellationToken); - - var sourceImageResult = await GetSourceImageAsync(message.BucketName, message.KeyName, cancellationToken); - if (!sourceImageResult.IsSuccess) - { - MarkAllAsFailed(jobOperations, $"Failed to retrieve source image from storage, ResultState: {sourceImageResult.Status}"); - await database.SaveChangesAsync(cancellationToken); - return; - } - - using var sourceImage = sourceImageResult.Value; - - foreach (var operationItem in jobOperations) - { - await ProcessOperationAsync( - operationItem, - message.BucketName, - sourceImage, - cancellationToken); - - await database.SaveChangesAsync(cancellationToken); - } - } - catch (Exception exception) - { - logger.LogError(exception, "An error occurred while consuming the ResizePictureConsumer message."); - throw; - } - } - - async Task GetOrCreateJobAsync(ResizePictureEvent message, CancellationToken cancellationToken) - { - var job = await database.Jobs - .SingleOrDefaultAsync(j => j.Id == message.JobId, cancellationToken); - - if (job is not null) - { - return job; - } - - var payloadJson = JsonSerializer.Serialize(message.Arguments); - job = new Job - { - Id = message.JobId, - JobType = JobType.ResizePicture, - Payload = payloadJson - }; - - await database.Jobs.AddAsync(job, cancellationToken); - return job; - } - - async Task> CreateJobOperationAsync(IReadOnlyCollection resizeItems, Guid jobId, - CancellationToken cancellationToken) - { - var jobOperations = new List(resizeItems.Count); - - foreach (var resizeItem in resizeItems) - { - var jobOperation = new JobOperation - { - JobId = jobId - }; - - await database.JobOperations.AddAsync(jobOperation, cancellationToken); - jobOperations.Add(new JobOperationItem(resizeItem, jobOperation)); - } - - return jobOperations; - } - - async Task> GetSourceImageAsync(string bucketName, string keyName, CancellationToken cancellationToken) - { - var fileResult = await storageHandler.GetFileAsync(bucketName, keyName, cancellationToken); - - if (fileResult.IsNotFound()) - { - logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", bucketName, keyName); - return Result.NotFound(); - } - - if (!fileResult.IsSuccess) - { - logger.LogError("Failed to read source image from storage: BucketName={BucketName}, KeyName={KeyName}. Errors: {Errors}", - bucketName, - keyName, - string.Join("; ", fileResult.Errors)); - return Result.Error(string.Join("; ", fileResult.Errors)); - } - - using var fileStream = fileResult.Value; - var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream, cancellationToken); - memoryStream.Position = 0; - - return Result.Success(memoryStream); - } - - async Task ProcessOperationAsync( - JobOperationItem operationItem, - string bucketName, - MemoryStream sourceImage, - CancellationToken cancellationToken) - { - var jobOperation = operationItem.JobOperation; - - try - { - jobOperation.JobState = JobState.InProgress; - sourceImage.Position = 0; - - var resizedPictureResult = await pictureHandler.ResizePictureAsync( - sourceImage, - operationItem.Item.Width, - operationItem.Item.Height, - cancellationToken); - - if (!resizedPictureResult.IsSuccess) - { - var errorMessage = string.Join("; ", resizedPictureResult.Errors); - logger.LogError("Failed to resize picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", - jobOperation.Id, - errorMessage); - - MarkAsFailed(jobOperation, errorMessage); - return; - } - - var saveResult = await storageHandler.UploadFileAsync( - resizedPictureResult.Value, - "image/webp", - bucketName, - $"{operationItem.Item.SaveOnRoute}/{Guid.NewGuid() }.webp", - cancellationToken); - - if (!saveResult.IsSuccess) - { - var errorMessage = string.Join("; ", saveResult.Errors); - logger.LogError("Failed to upload resized picture for JobOperationId={JobOperationId}. Error: {ErrorMessage}", - jobOperation.Id, - errorMessage); - - MarkAsFailed(jobOperation, errorMessage); - return; - } - - jobOperation.JobState = JobState.Completed; - jobOperation.FinishedAt = DateTime.UtcNow; - } - catch (Exception exception) - { - logger.LogError(exception, - "An error occurred while processing resize for JobOperationId={JobOperationId}, Width={Width}, Height={Height}, SaveOnRoute={SaveOnRoute}", - jobOperation.Id, - operationItem.Item.Width, - operationItem.Item.Height, - operationItem.Item.SaveOnRoute); - - MarkAsFailed(jobOperation, exception.Message); - } - } - - static void MarkAllAsFailed(IEnumerable jobOperations, string errorMessage) - { - foreach (var operationItem in jobOperations) - { - MarkAsFailed(operationItem.JobOperation, errorMessage); - } - } - - static void MarkAsFailed(JobOperation jobOperation, string errorMessage) - { - jobOperation.JobState = JobState.Failed; - jobOperation.FinishedAt = DateTime.UtcNow; - jobOperation.ErrorMessage = errorMessage; - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs new file mode 100644 index 0000000..e3e6ada --- /dev/null +++ b/Infrastructure/Messaging/Consumers/UpdatePlayListPictureConsumer.cs @@ -0,0 +1,82 @@ +using MassTransit; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; +using Musify.Application.Events; +using Musify.Infrastructure.Messaging.Activities.Arguments; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class UpdatePlayListPictureConsumer(IBus bus, IPictureHandler pictureHandler, + StorageSettings storageSettings, PlayListConfiguration playListConfiguration) + : IConsumer + { + public const string QueueName = "Update-PlayList-Picture"; + + public async Task Consume(ConsumeContext consumeContext) + { + var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid()); + + var smallPictureKeyName = Path.Combine( + playListConfiguration.Routes.SmallPictures, + $"{consumeContext.Message.PlayListId}_small.{pictureHandler.FileExtension}"); + var mediumPictureKeyName = Path.Combine( + playListConfiguration.Routes.MediumPictures, + $"{consumeContext.Message.PlayListId}_medium.{pictureHandler.FileExtension}"); + var largePictureKeyName = Path.Combine( + playListConfiguration.Routes.LargePictures, + $"{consumeContext.Message.PlayListId}_large.{pictureHandler.FileExtension}"); + + routingSlipBuilder.AddActivity( + "ResizeSmall", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.SmallPictures, + playListConfiguration.PicturesSizes.SmallPictureWidth, + playListConfiguration.PicturesSizes.SmallPictureHeight)); + + routingSlipBuilder.AddActivity( + "ResizeMedium", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.MediumPictures, + playListConfiguration.PicturesSizes.MediumPictureWidth, + playListConfiguration.PicturesSizes.MediumPictureHeight)); + + routingSlipBuilder.AddActivity( + "ResizeLarge", + new Uri("Queue:Resize-Picture_Execute"), + new ResizePictureArgument( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName, + storageSettings.BucketName, + playListConfiguration.Routes.LargePictures, + playListConfiguration.PicturesSizes.LargePictureWidth, + playListConfiguration.PicturesSizes.LargePictureHeight)); + + routingSlipBuilder.AddActivity( + "DeleteOriginal", + new Uri("Queue:Delete-Picture_Execute"), + new RemoveFileArguments( + storageSettings.BucketName, + consumeContext.Message.OriginalPictureKeyName)); + + routingSlipBuilder.AddActivity( + "UpdatePlayListPicture", + new Uri("Queue:Update-PlayList-Picture_Execute"), + new UpdatePlayListPictureArguments( + consumeContext.Message.PlayListId, + smallPictureKeyName, + mediumPictureKeyName, + largePictureKeyName)); + + var routingSlip = routingSlipBuilder.Build(); + await bus.Execute(routingSlip); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index def4d94..f44ddcf 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -38,8 +38,7 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddMassTransit(options => { - options.AddConsumer(); - options.AddConsumer(); + options.AddConsumer(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -60,13 +59,9 @@ namespace Musify.Infrastructure.Messaging options.Password(password); } ); - busFactoryConfigurator.ReceiveEndpoint(ResizePictureConsumer.QueueName, endpointConfigurator => + busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); - } ); - busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator => - { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); + endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); } ); } ); diff --git a/Infrastructure/Pictures/PictureHandler.cs b/Infrastructure/Pictures/PictureHandler.cs index 1e5db72..4185761 100644 --- a/Infrastructure/Pictures/PictureHandler.cs +++ b/Infrastructure/Pictures/PictureHandler.cs @@ -9,6 +9,10 @@ namespace Musify.Infrastructure.Pictures public class PictureHandler(ILogger logger) : IPictureHandler { + public string FileExtension => "webp"; + + public string ContentType => "image/webp"; + public async Task> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken) { try diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index a7b9e28..d6a42cf 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -142,5 +142,31 @@ namespace Musify.Infrastructure.Storage return Result.Success(keyName); } + + public async Task CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName, + CancellationToken cancellationToken) + { + var request = new CopyObjectRequest + { + SourceBucket = sourceBucketName, + SourceKey = sourceKeyName, + DestinationBucket = destinationBucketName, + DestinationKey = destinationKeyName + }; + + try + { + var response = await amazonS3.CopyObjectAsync(request, cancellationToken); + logger.LogInformation("Successfully copied file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", + sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); + return Result.Success(); + } + catch + { + logger.LogError("Failed to copy file from S3 with bucket name {SourceBucketName} and key name {SourceKeyName} to bucket name {DestinationBucketName} and key name {DestinationKeyName}", + sourceBucketName, sourceKeyName, destinationBucketName, destinationKeyName); + return Result.Error($"Failed to copy file from S3 with bucket name {sourceBucketName} and key name {sourceKeyName} to bucket name {destinationBucketName} and key name {destinationKeyName}"); + } + } } } \ No newline at end of file