From 34c213ef5a4b234e17639006d44c8ce1c79096b3 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 19 Mar 2026 01:09:02 +0100 Subject: [PATCH] jobs and others --- Application/Application.csproj | 2 +- .../Contracts/Application/IPictureService.cs | 12 -- .../Contracts/Infrastructure/IDatabase.cs | 2 + Application/Events/RemoveFileEvent.cs | 6 + Application/Events/ResizePictureEvent.cs | 7 + Application/Events/ResizePictureItems.cs | 7 + .../Commands/ResizePicture/ResizeCommand.cs | 16 -- .../ResizePicture/ResizeCommandHandler.cs | 18 --- .../Pictures/Contracts/PictureResize.cs | 7 - .../Pictures/Events/PictureResizeEvent.cs | 8 - .../Pictures/PictureDependencyInjection.cs | 13 -- Application/Pictures/PictureService.cs | 61 -------- .../CreatePlayList/CreatePlayListCommand.cs | 1 + .../CreatePlayListCommandHandler.cs | 47 +++--- .../DeletePlayList/DeletePlayListCommand.cs | 8 + .../DeletePlayListCommandHandler.cs | 36 +++++ Domain/Entities/EntityType.cs | 9 -- Domain/Entities/Job.cs | 29 ++++ Domain/Entities/JobState.cs | 10 ++ Domain/Entities/JobType.cs | 8 + Domain/Entities/Upload.cs | 8 +- Domain/Entities/UploadState.cs | 5 +- .../Consumers/PictureResizerConsumer.cs | 138 ------------------ .../Messaging/Consumers/RemoveFileConsumer.cs | 14 ++ .../Consumers/ResizePictureConsumer.cs | 138 ++++++++++++++++++ .../MassTransitDependencyInjection.cs | 6 +- Infrastructure/Persistence/Database.cs | 2 + Infrastructure/Storage/StorageHandler.cs | 32 +++- 28 files changed, 334 insertions(+), 316 deletions(-) delete mode 100644 Application/Contracts/Application/IPictureService.cs create mode 100644 Application/Events/RemoveFileEvent.cs create mode 100644 Application/Events/ResizePictureEvent.cs create mode 100644 Application/Events/ResizePictureItems.cs delete mode 100644 Application/Pictures/Commands/ResizePicture/ResizeCommand.cs delete mode 100644 Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs delete mode 100644 Application/Pictures/Contracts/PictureResize.cs delete mode 100644 Application/Pictures/Events/PictureResizeEvent.cs delete mode 100644 Application/Pictures/PictureDependencyInjection.cs delete mode 100644 Application/Pictures/PictureService.cs create mode 100644 Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs create mode 100644 Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs delete mode 100644 Domain/Entities/EntityType.cs create mode 100644 Domain/Entities/Job.cs create mode 100644 Domain/Entities/JobState.cs create mode 100644 Domain/Entities/JobType.cs delete mode 100644 Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs create mode 100644 Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs diff --git a/Application/Application.csproj b/Application/Application.csproj index abf9700..f9b7155 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -23,7 +23,7 @@ - + diff --git a/Application/Contracts/Application/IPictureService.cs b/Application/Contracts/Application/IPictureService.cs deleted file mode 100644 index b8bab3a..0000000 --- a/Application/Contracts/Application/IPictureService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ardalis.Result; -using Musify.Application.Pictures.Contracts; -using Musify.Domain.Entities; - -namespace Musify.Application.Contracts.Application -{ - public interface IPictureService - { - Task> ResizePictureAsync(EntityType entityType, Guid EntityId, string keyName, string contentType, Stream pictureStream, - PictureResize[] pictureResizes, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 172bfd8..69ea4eb 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -15,6 +15,8 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Uploads { get; } + DbSet Jobs { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs new file mode 100644 index 0000000..76fc108 --- /dev/null +++ b/Application/Events/RemoveFileEvent.cs @@ -0,0 +1,6 @@ +namespace Musify.Application.Events +{ + public record RemoveFileEvent( + string BucketName, + string KeyName); +} \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs new file mode 100644 index 0000000..00ccfce --- /dev/null +++ b/Application/Events/ResizePictureEvent.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Events +{ + public record ResizePictureEvent( + string BucketName, + string KeyName, + ResizePictureItems[] Items); +} \ No newline at end of file diff --git a/Application/Events/ResizePictureItems.cs b/Application/Events/ResizePictureItems.cs new file mode 100644 index 0000000..6af2dae --- /dev/null +++ b/Application/Events/ResizePictureItems.cs @@ -0,0 +1,7 @@ +namespace Musify.Application.Events +{ + public record ResizePictureItems( + int Width, + int Height, + string SaveOnRoute); +} \ No newline at end of file diff --git a/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs deleted file mode 100644 index 707ed8d..0000000 --- a/Application/Pictures/Commands/ResizePicture/ResizeCommand.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Ardalis.Result; -using DispatchR.Abstractions.Send; -using Musify.Application.Pictures.Contracts; -using Musify.Domain.Entities; - -namespace Musify.Application.Pictures.Commands.ResizePicture -{ - public record ResizeCommand( - EntityType EntityType, - Guid EntityId, - string KeyName, - string ContentType, - Stream PictureStream, - PictureResize[] PictureResizes) - : IRequest>; -} \ No newline at end of file diff --git a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs b/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs deleted file mode 100644 index bd61a9c..0000000 --- a/Application/Pictures/Commands/ResizePicture/ResizeCommandHandler.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Ardalis.Result; -using DispatchR.Abstractions.Send; -using Musify.Application.Contracts.Application; - -namespace Musify.Application.Pictures.Commands.ResizePicture -{ - public class ResizeCommandHandler(IPictureService pictureService) - : IRequestHandler >> - { - public async Task> Handle(ResizeCommand request, CancellationToken cancellationToken) - { - var result = await pictureService.ResizePictureAsync(request.EntityType, request.EntityId, request.KeyName, request.ContentType, - request.PictureStream, request.PictureResizes, cancellationToken); - - return result; - } - } -} \ No newline at end of file diff --git a/Application/Pictures/Contracts/PictureResize.cs b/Application/Pictures/Contracts/PictureResize.cs deleted file mode 100644 index f66021a..0000000 --- a/Application/Pictures/Contracts/PictureResize.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Musify.Application.Pictures.Contracts -{ - public record PictureResize( - int Width, - int Height, - string SaveOnRoute); -} \ No newline at end of file diff --git a/Application/Pictures/Events/PictureResizeEvent.cs b/Application/Pictures/Events/PictureResizeEvent.cs deleted file mode 100644 index 8439e14..0000000 --- a/Application/Pictures/Events/PictureResizeEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Musify.Application.Pictures.Contracts; - -namespace Musify.Application.Pictures.Events -{ - public record PictureResizeEvent( - Guid UploadId, - PictureResize[] ImageResizes); -} \ No newline at end of file diff --git a/Application/Pictures/PictureDependencyInjection.cs b/Application/Pictures/PictureDependencyInjection.cs deleted file mode 100644 index 914897f..0000000 --- a/Application/Pictures/PictureDependencyInjection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Contracts.Application; - -namespace Musify.Application.Pictures -{ - public static class PictureDependencyInjection - { - public static void AddPictureService(this IServiceCollection serviceDescriptors) - { - serviceDescriptors.AddScoped(); - } - } -} \ No newline at end of file diff --git a/Application/Pictures/PictureService.cs b/Application/Pictures/PictureService.cs deleted file mode 100644 index 0dfcedc..0000000 --- a/Application/Pictures/PictureService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Ardalis.Result; -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; -using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Pictures.Commands.ResizePicture; -using Musify.Application.Pictures.Contracts; -using Musify.Application.Pictures.Events; -using Musify.Domain.Entities; - -namespace Musify.Application.Pictures -{ - public class PictureService(IStorageHandler storageHandler, IDatabase database, IPublishEndpoint publishEndpoint, - ILogger logger, StorageConfiguration storageConfiguration) - : IPictureService - { - public async Task> ResizePictureAsync(EntityType entityType, Guid EntityId, string keyName, string contentType, Stream pictureStream, - PictureResize[] pictureResizes, CancellationToken cancellationToken) - { - var pictureId = Guid.NewGuid(); - - var upload = new Upload - { - Id = pictureId, - KeyName = keyName, - ContentType = contentType, - BucketName = storageConfiguration.BucketName, - EntityType = entityType, - EntityId = EntityId, - State = UploadState.Completed - }; - - await database.Uploads.AddAsync(upload, cancellationToken); - - await database.SaveChangesAsync(cancellationToken); - - var uploadResult = await storageHandler.UploadFileAsync( - pictureStream, - contentType, - storageConfiguration.BucketName, - keyName, - cancellationToken); - - if (!uploadResult.IsSuccess) - { - logger.LogError("Failed to upload the picture with ID {PictureId} to bucket {BucketName}.", pictureId, storageConfiguration.BucketName); - - upload.State = UploadState.Failed; - database.Uploads.Update(upload); - await database.SaveChangesAsync(cancellationToken); - - return Result.Error("Failed to upload the picture."); - } - - await publishEndpoint.Publish(new PictureResizeEvent(pictureId, pictureResizes)); - - return Result.Success(); - } - } -} \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs index b55d20a..d813000 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommand.cs @@ -9,6 +9,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList string Name, string Description, Stream PictureStream, + string PictureFileType, string PictureContentType) : IRequest>; } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index 5127c3b..d47cd82 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -1,18 +1,18 @@ using Ardalis.Result; using DispatchR.Abstractions.Send; +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; -using Musify.Application.Contracts.Application; using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Pictures.Contracts; +using Musify.Application.Events; using Musify.Application.PlayLists.Contracts; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Commands.CreatePlayList { - public class CreatePlayListCommandHandler(IPictureService pictureService, IDatabase database, ILogger logger, - PlayListConfiguration playListConfiguration) + public class CreatePlayListCommandHandler(IPublishEndpoint publishEndpoint, IDatabase database, IStorageHandler storageHandler, + ILogger logger, StorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration) : IRequestHandler> > { public async Task> Handle(CreatePlayListCommand request, CancellationToken cancellationToken) @@ -35,34 +35,45 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList MediumPictureKeyName = playListConfiguration.Routes.MediumPictures, LargePictureKeyName = playListConfiguration.Routes.LargePictures, }; - await database.PlayLists.AddAsync(playList, cancellationToken); - await database.SaveChangesAsync(cancellationToken); + var imageId = Guid.NewGuid(); + + var imageKeyName = $"{playListConfiguration.Routes.OriginalPictures}/{imageId}.{request.PictureFileType}"; + + var uploadResult = await storageHandler.UploadFileAsync( + request.PictureStream, + request.PictureContentType, + storageConfiguration.BucketName, + imageKeyName, + 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."); + } logger.LogInformation("PlayList with Id={PlayListId} created successfully for UserId={UserId}.", playList.Id, request.UserId); - await pictureService.ResizePictureAsync( - EntityType.PlayList, - playList.Id, - playList.Id.ToString(), - request.PictureContentType, - request.PictureStream, - [ - new PictureResize( + await publishEndpoint.Publish(new ResizePictureEvent( + storageConfiguration.BucketName, + imageKeyName, + [ + new ResizePictureItems( playListConfiguration.PicturesSizes.SmallPictureWidth, playListConfiguration.PicturesSizes.SmallPictureHeight, playListConfiguration.Routes.SmallPictures), - new PictureResize( + new ResizePictureItems( playListConfiguration.PicturesSizes.MediumPictureWidth, playListConfiguration.PicturesSizes.MediumPictureHeight, playListConfiguration.Routes.MediumPictures), - new PictureResize( + new ResizePictureItems( playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ], - cancellationToken); + ] ), cancellationToken); logger.LogInformation("Picture for PlayList with Id={PlayListId} ", playList.Id); diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs new file mode 100644 index 0000000..50d4cad --- /dev/null +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommand.cs @@ -0,0 +1,8 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; + +namespace Musify.Application.PlayLists.Commands.DeletePlayList +{ + public record DeletePlayListCommand(Guid UserId, Guid PlayListId) + : IRequest; +} \ No newline at end of file diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs new file mode 100644 index 0000000..e7ca9b7 --- /dev/null +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -0,0 +1,36 @@ +using Ardalis.Result; +using DispatchR.Abstractions.Send; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Application.Contracts.Infrastructure; + +namespace Musify.Application.PlayLists.Commands.DeletePlayList +{ + public class DeletePlayListCommandHandler(IDatabase database, IStorageHandler storageHandler, + StorageConfiguration storageConfiguration, ILogger logger) + : IRequestHandler> + { + public async Task Handle(DeletePlayListCommand request, CancellationToken cancellationToken) + { + var playList = await database.PlayLists.FindAsync(request.PlayListId, cancellationToken); + + if (playList is null) + { + logger.LogDebug("PlayList with id={PlayListId} not found", request.PlayListId); + return Result.NotFound(); + } + + if (playList.UserId != request.UserId) + { + logger.LogInformation("PlayList with id={PlayListId} does not belong to user with id={UserId}", request.PlayListId, request.UserId); + return Result.Unauthorized(); + } + + database.PlayLists.Remove(playList); + + await database.SaveChangesAsync(cancellationToken); + + return Result.NoContent(); + } + } +} diff --git a/Domain/Entities/EntityType.cs b/Domain/Entities/EntityType.cs deleted file mode 100644 index 24c2cd9..0000000 --- a/Domain/Entities/EntityType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Musify.Domain.Entities -{ - public enum EntityType - { - User, - Track, - PlayList - } -} \ No newline at end of file diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs new file mode 100644 index 0000000..6c20331 --- /dev/null +++ b/Domain/Entities/Job.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ + [Table("Job")] + public class Job + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required JobType JobType { get; set; } + + [Required] + public JobState JobState { get; set; } = JobState.Pending; + + [Required] + public required string Payload { get; set; } + + [Required] + public DateTime CreateAt { get; set; } = DateTime.UtcNow; + + [Required] + public int RetryCount { get; set; } = 0; + + public DateTime CompletedAt { get; set; } + } +} \ No newline at end of file diff --git a/Domain/Entities/JobState.cs b/Domain/Entities/JobState.cs new file mode 100644 index 0000000..9cc6cde --- /dev/null +++ b/Domain/Entities/JobState.cs @@ -0,0 +1,10 @@ +namespace Musify.Domain.Entities +{ + public enum JobState + { + Pending, + Failed, + InProgress, + Completed + } +} \ No newline at end of file diff --git a/Domain/Entities/JobType.cs b/Domain/Entities/JobType.cs new file mode 100644 index 0000000..c0ded2f --- /dev/null +++ b/Domain/Entities/JobType.cs @@ -0,0 +1,8 @@ +namespace Musify.Domain.Entities +{ + public enum JobType + { + ResizePicture, + RemoveFile, + } +} \ No newline at end of file diff --git a/Domain/Entities/Upload.cs b/Domain/Entities/Upload.cs index 0bb9ca4..a74db25 100644 --- a/Domain/Entities/Upload.cs +++ b/Domain/Entities/Upload.cs @@ -19,15 +19,9 @@ namespace Musify.Domain.Entities public required string ContentType { get; set; } [Required] - public required EntityType EntityType { get; set; } - - [Required] - public required Guid EntityId { get; set; } + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; [Required] public UploadState State { get; set; } = UploadState.Pending; - - [Required] - public DateTime CreatedTime { get; set; } = DateTime.UtcNow; } } \ No newline at end of file diff --git a/Domain/Entities/UploadState.cs b/Domain/Entities/UploadState.cs index 738b5d4..50a4dfc 100644 --- a/Domain/Entities/UploadState.cs +++ b/Domain/Entities/UploadState.cs @@ -3,8 +3,7 @@ public enum UploadState { Pending, - Processing, - Completed, - Failed + Failed, + Successful, } } \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs b/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs deleted file mode 100644 index 28b6bdb..0000000 --- a/Infrastructure/Messaging/Consumers/PictureResizerConsumer.cs +++ /dev/null @@ -1,138 +0,0 @@ -using Ardalis.Result; -using MassTransit; -using Microsoft.Extensions.Logging; -using Musify.Application.Contracts.Infrastructure; -using Musify.Application.Pictures.Contracts; -using Musify.Application.Pictures.Events; -using Musify.Domain.Entities; - -namespace Musify.Infrastructure.Messaging.Consumers -{ - public class PictureResizerConsumer(IStorageHandler storageHandler, IDatabase database, IPictureHandler pictureHandler, ILogger logger) - : IConsumer - { - public const string QueueName = "picture-resize-queue"; - - public async Task Consume(ConsumeContext consumeContext) - { - try - { - var upload = await database.Uploads.FindAsync(consumeContext.Message.UploadId); - - if (upload is null) - { - logger.LogWarning("Upload not found in database: UploadId={UploadId}", consumeContext.Message.UploadId); - return; - } - - if (upload.State != UploadState.Completed) - { - logger.LogInformation("Upload is not in a Completed state, skipping processing: UploadId={UploadId}, State={State}", consumeContext.Message.UploadId, upload.State); - return; - } - - await UpdateState(database, upload, UploadState.Processing, consumeContext.CancellationToken); - - var fileResult = await storageHandler.GetFileAsync( - upload.BucketName, - upload.KeyName, - consumeContext.CancellationToken); - - if (fileResult.IsNotFound()) - { - logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", upload.BucketName, upload.KeyName); - await UpdateState(database, upload, UploadState.Failed, consumeContext.CancellationToken); - return; - } - - using var fileStream = fileResult.Value; - - using var memoryStream = new MemoryStream(); - await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken); - - foreach (var resize in consumeContext.Message.ImageResizes) - { - var newUploadId = Guid.NewGuid(); - - var newUpload = new Upload - { - Id = newUploadId, - BucketName = upload.BucketName, - KeyName = $"{resize.SaveOnRoute}/{newUploadId}.webp", - ContentType = "image/webp", - State = UploadState.Processing, - EntityId = upload.EntityId, - EntityType = upload.EntityType - }; - - await database.Uploads.AddAsync(newUpload, consumeContext.CancellationToken); - - await database.SaveChangesAsync(consumeContext.CancellationToken); - - try - { - await ResizePicture(upload, newUpload, memoryStream, resize, consumeContext.CancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", upload.Id, resize); - await UpdateState(database, newUpload, UploadState.Failed, consumeContext.CancellationToken); - continue; - } - } - - await UpdateState(database, upload, UploadState.Completed, consumeContext.CancellationToken); - } - catch (Exception exception) - { - logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message."); - throw; - } - finally - { - logger.LogInformation("Saving changes to database after processing ImageResizedConsumer message."); - await database.SaveChangesAsync(consumeContext.CancellationToken); - } - } - - async Task UpdateState(IDatabase database, Upload upload, UploadState uploadState, CancellationToken cancellationToken) - { - upload.State = uploadState; - database.Uploads.Update(upload); - } - - async Task ResizePicture(Upload originalUpload, Upload newUpload, MemoryStream memoryStream, PictureResize pictureResize, CancellationToken cancellationToken) - { - memoryStream.Position = 0; - - var pictureResult = await pictureHandler.ResizePictureAsync( - memoryStream, - pictureResize.Width, - pictureResize.Height, - cancellationToken); - - if (!pictureResult.IsSuccess) - { - logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); - await UpdateState(database, newUpload, UploadState.Failed, cancellationToken); - return; - } - - var saveResult = await storageHandler.UploadFileAsync( - pictureResult.Value, - "image/webp", - originalUpload.BucketName, - $"{pictureResize.SaveOnRoute}/{newUpload.Id}.webp", - cancellationToken); - - if (!saveResult.IsSuccess) - { - logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); - await UpdateState(database, newUpload, UploadState.Failed, cancellationToken); - return; - } - - await UpdateState(database, newUpload, UploadState.Completed, cancellationToken); - } - } -} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs new file mode 100644 index 0000000..29b4244 --- /dev/null +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -0,0 +1,14 @@ +using MassTransit; +using Musify.Application.Events; + +namespace Musify.Infrastructure.Messaging.Consumers +{ + public class RemoveFileConsumer + : IConsumer + { + public Task Consume(ConsumeContext context) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs new file mode 100644 index 0000000..3c5ed0f --- /dev/null +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -0,0 +1,138 @@ +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 + { + public const string QueueName = "picture-resize-queue"; + + public async Task Consume(ConsumeContext consumeContext) + { + try + { + Dictionary jobs = new Dictionary(); + + foreach (var item in consumeContext.Message.Items) + { + var payloadJson = JsonSerializer.Serialize(item); + + var job = await database.Jobs + .SingleOrDefaultAsync(job => job.JobType == JobType.ResizePicture && job.Payload == payloadJson, consumeContext.CancellationToken); + + if (job is null) + { + job = new Job + { + JobType = JobType.ResizePicture, + Payload = payloadJson, + }; + await database.Jobs.AddAsync(job, consumeContext.CancellationToken); + } + else + { + job.RetryCount++; + job.JobState = JobState.InProgress; + database.Jobs.Update(job); + } + + jobs.Add(item, job); + } + + var fileResult = await storageHandler.GetFileAsync( + consumeContext.Message.BucketName, + consumeContext.Message.KeyName, + consumeContext.CancellationToken); + + if (fileResult.IsNotFound()) + { + logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", + consumeContext.Message.BucketName, consumeContext.Message.KeyName); + + foreach (var job in jobs) + { + job.Value.JobState = JobState.Failed; + } + await database.SaveChangesAsync(consumeContext.CancellationToken); + return; + } + + using var fileStream = fileResult.Value; + using var memoryStream = new MemoryStream(); + await fileStream.CopyToAsync(memoryStream, consumeContext.CancellationToken); + + foreach (var item in consumeContext.Message.Items) + { + var job = jobs[item]; + + try + { + await ResizePicture( + job, + consumeContext.Message.BucketName, + memoryStream, + item, + consumeContext.CancellationToken); + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", job.Id, item); + continue; + } + } + } + catch (Exception exception) + { + logger.LogError(exception, "An error occurred while consuming the ImageResizedConsumer message."); + throw; + } + finally + { + logger.LogInformation("Saving changes to database after processing ImageResizedConsumer message."); + await database.SaveChangesAsync(consumeContext.CancellationToken); + } + } + + async Task ResizePicture(Job job, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) + { + memoryStream.Position = 0; + job.JobState = JobState.InProgress; + + var pictureResult = await pictureHandler.ResizePictureAsync( + memoryStream, + items.Width, + items.Height, + cancellationToken); + + if (!pictureResult.IsSuccess) + { + logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); + job.JobState = JobState.Failed; + return; + } + + var saveResult = await storageHandler.UploadFileAsync( + pictureResult.Value, + "image/webp", + bucketName, + $"{items.SaveOnRoute}/{Guid.NewGuid() }.webp", + cancellationToken); + + if (!saveResult.IsSuccess) + { + logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); + job.JobState = JobState.Failed; + return; + } + + job.JobState = JobState.Completed; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/MassTransitDependencyInjection.cs b/Infrastructure/Messaging/MassTransitDependencyInjection.cs index c7a37f0..a38f593 100644 --- a/Infrastructure/Messaging/MassTransitDependencyInjection.cs +++ b/Infrastructure/Messaging/MassTransitDependencyInjection.cs @@ -37,7 +37,7 @@ namespace Musify.Infrastructure.Messaging { serviceDescriptors.AddMassTransit(options => { - options.AddConsumer(); + options.AddConsumer(); options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) => { @@ -58,9 +58,9 @@ namespace Musify.Infrastructure.Messaging options.Password(password); } ); - busFactoryConfigurator.ReceiveEndpoint(PictureResizerConsumer.QueueName, endpointConfigurator => + busFactoryConfigurator.ReceiveEndpoint(ResizePictureConsumer.QueueName, endpointConfigurator => { - endpointConfigurator.ConfigureConsumer(busRegistrationContext); + endpointConfigurator.ConfigureConsumer(busRegistrationContext); } ); } ); } ); diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 5a1e8da..5148f5a 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -22,5 +22,7 @@ namespace Musify.Infrastructure.Persistence public DbSet PlayListHasTracks => Set(); public DbSet Uploads => Set(); + + public DbSet Jobs => Set(); } } \ No newline at end of file diff --git a/Infrastructure/Storage/StorageHandler.cs b/Infrastructure/Storage/StorageHandler.cs index ff6a7ac..c324220 100644 --- a/Infrastructure/Storage/StorageHandler.cs +++ b/Infrastructure/Storage/StorageHandler.cs @@ -4,10 +4,11 @@ using Amazon.S3.Transfer; using Ardalis.Result; using Microsoft.Extensions.Logging; using Musify.Application.Contracts.Infrastructure; +using Musify.Domain.Entities; namespace Musify.Infrastructure.Storage { - public class StorageHandler(IAmazonS3 amazonS3, ILogger logger) + public class StorageHandler(IDatabase database, IAmazonS3 amazonS3, ILogger logger) : IStorageHandler { public async Task> GetFileAsync(string bucketName, string keyName, CancellationToken cancellationToken) @@ -75,18 +76,31 @@ namespace Musify.Infrastructure.Storage string fileName = Path.GetFileName(file); string filePath = Path.Combine(sourceDirectory, file); + var upload = new Upload + { + Id = Guid.NewGuid(), + BucketName = bucketName, + KeyName = $"{route}/{fileName}", + ContentType = "Application/Octet-Stream" + }; + await database.Uploads.AddAsync(upload, cancellationToken); + try { await trasnsferUtility.UploadAsync(filePath, bucketName, fileName, cancellationToken); + upload.State = UploadState.Successful; logger.LogInformation("Successfully transferred file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); } catch { logger.LogError("Failed to transfer file {FileName} to S3 with bucket name {BucketName} and route {Route}", fileName, bucketName, route); - return Result.Error($"Failed to transfer file {fileName} to S3 with bucket name {bucketName} and route {route}"); + upload.State = UploadState.Failed; } } + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Successfully saved upload records to database for all files transferred to S3 with bucket name {BucketName} and route {Route}", bucketName, route); + return Result.Success(); } @@ -100,6 +114,15 @@ namespace Musify.Infrastructure.Storage Key = keyName }; + var upload = new Upload + { + Id = Guid.NewGuid(), + BucketName = bucketName, + KeyName = keyName, + ContentType = contentType + }; + await database.Uploads.AddAsync(upload, cancellationToken); + try { var response = await amazonS3.PutObjectAsync(request, cancellationToken); @@ -108,9 +131,14 @@ namespace Musify.Infrastructure.Storage catch { logger.LogError("Failed to upload file to S3 with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + upload.State = UploadState.Failed; return Result.Error($"Failed to upload file to S3 with bucket name {bucketName} and key name {keyName}"); } + upload.State = UploadState.Successful; + await database.SaveChangesAsync(cancellationToken); + logger.LogInformation("Successfully saved upload record to database with bucket name {BucketName} and key name {KeyName}", bucketName, keyName); + return Result.Success(keyName); } }