Changes
This commit is contained in:
@@ -4,6 +4,10 @@ namespace Musify.Application.Contracts.Infrastructure
|
|||||||
{
|
{
|
||||||
public interface IPictureHandler
|
public interface IPictureHandler
|
||||||
{
|
{
|
||||||
|
public string FileExtension { get; }
|
||||||
|
|
||||||
|
public string ContentType { get; }
|
||||||
|
|
||||||
Task<Result<Stream>> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken);
|
Task<Result<Stream>> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,8 @@ namespace Musify.Application.Contracts.Infrastructure
|
|||||||
Task<Result> TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken);
|
Task<Result> TransferFilesAsync(string sourceDirectory, string bucketName, string route, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task<Result> RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken);
|
Task<Result> RemoveFileAsync(string bucketName, string keyName, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Result> CopyFileAsync(string sourceBucketName, string sourceKeyName, string destinationBucketName, string destinationKeyName,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Musify.Application.Events
|
|
||||||
{
|
|
||||||
public record ResizePictureArguments(
|
|
||||||
int Width,
|
|
||||||
int Height,
|
|
||||||
string SaveOnRoute);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Application.Events
|
||||||
|
{
|
||||||
|
public record UpdatePlayListPictureEvent(
|
||||||
|
Guid PlayListId,
|
||||||
|
string OriginalPictureKeyName);
|
||||||
|
}
|
||||||
@@ -60,24 +60,7 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await eventBus.PublishAsync(new ResizePictureEvent(
|
await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -138,25 +138,7 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await eventBus.PublishAsync(new ResizePictureEvent(
|
await eventBus.PublishAsync(new UpdatePlayListPictureEvent(playList.Id, playList.OriginalPictureKeyName), cancellationToken);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ namespace Musify.Infrastructure.Configuration
|
|||||||
serviceDescriptors
|
serviceDescriptors
|
||||||
.AddValidatedConfiguration<DatabaseConfiguration>(configuration, DatabaseConfiguration.SectionName)
|
.AddValidatedConfiguration<DatabaseConfiguration>(configuration, DatabaseConfiguration.SectionName)
|
||||||
.AddValidatedConfiguration<KeycloakConfiguration>(configuration, KeycloakConfiguration.SectionName)
|
.AddValidatedConfiguration<KeycloakConfiguration>(configuration, KeycloakConfiguration.SectionName)
|
||||||
.AddValidatedConfiguration<StorageClientConfiguration>(configuration, StorageClientConfiguration.SectionName);
|
.AddValidatedConfiguration<StorageClientConfiguration>(configuration, StorageClientConfiguration.SectionName)
|
||||||
|
.AddValidatedConfiguration<WorkerStorageConfiguration>(configuration, WorkerStorageConfiguration.SectionName);
|
||||||
|
|
||||||
return serviceDescriptors;
|
return serviceDescriptors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ namespace Musify.Infrastructure.Configuration
|
|||||||
[MinLength(8)]
|
[MinLength(8)]
|
||||||
public required string SecretAccessKey { get; set; }
|
public required string SecretAccessKey { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
public bool ForcePathStyle { get; set; }
|
public bool ForcePathStyle { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
public bool UseHttp { get; set; }
|
public bool UseHttp { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Musify.Infrastructure.Messaging.Activities.Arguments
|
||||||
|
{
|
||||||
|
public record CopyFileArguments(
|
||||||
|
string SourceBucketName,
|
||||||
|
string SourceKeyName,
|
||||||
|
string DestinationBucketName,
|
||||||
|
string DestinationKeyName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Infrastructure.Messaging.Activities.Arguments
|
||||||
|
{
|
||||||
|
public record RemoveFileArguments(
|
||||||
|
string BucketName,
|
||||||
|
string KeyName);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Musify.Infrastructure.Messaging.Activities.Arguments
|
||||||
|
{
|
||||||
|
public record UpdatePlayListPictureArguments(
|
||||||
|
Guid PlayListId,
|
||||||
|
string SmallPictureKeyName,
|
||||||
|
string MediumPictureKeyName,
|
||||||
|
string LargePictureKeyName);
|
||||||
|
}
|
||||||
@@ -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<CopyFilesActivity> logger)
|
||||||
|
: IExecuteActivity<CopyFileArguments[]>
|
||||||
|
{
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<CopyFileArguments[]> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RemoveFilesActivity> logger)
|
||||||
|
: IExecuteActivity<RemoveFileArguments[]>
|
||||||
|
{
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<RemoveFileArguments[]> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ResizePictureArgument>
|
||||||
|
{
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<ResizePictureArgument> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<UpdatePlayListPictureArguments>
|
||||||
|
{
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<UpdatePlayListPictureArguments> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RemoveFileConsumer> logger)
|
|
||||||
: IConsumer<RemoveFileEvent>
|
|
||||||
{
|
|
||||||
public const string QueueName = "file-remove-queue";
|
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<RemoveFileEvent> 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<Job> 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<JobOperation> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<ResizePictureConsumer> logger)
|
|
||||||
: IConsumer<ResizePictureEvent>
|
|
||||||
{
|
|
||||||
private record JobOperationItem(ResizePictureArguments Item, JobOperation JobOperation);
|
|
||||||
|
|
||||||
public const string QueueName = "picture-resize-queue";
|
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<ResizePictureEvent> 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<Job> 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<List<JobOperationItem>> CreateJobOperationAsync(IReadOnlyCollection<ResizePictureArguments> resizeItems, Guid jobId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var jobOperations = new List<JobOperationItem>(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<Result<MemoryStream>> 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<JobOperationItem> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<UpdatePlayListPictureEvent>
|
||||||
|
{
|
||||||
|
public const string QueueName = "Update-PlayList-Picture";
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<UpdatePlayListPictureEvent> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,8 +38,7 @@ namespace Musify.Infrastructure.Messaging
|
|||||||
{
|
{
|
||||||
serviceDescriptors.AddMassTransit(options =>
|
serviceDescriptors.AddMassTransit(options =>
|
||||||
{
|
{
|
||||||
options.AddConsumer<ResizePictureConsumer>();
|
options.AddConsumer<UpdatePlayListPictureConsumer>();
|
||||||
options.AddConsumer<RemoveFileConsumer>();
|
|
||||||
|
|
||||||
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
|
options.UsingRabbitMq((busRegistrationContext, busFactoryConfigurator) =>
|
||||||
{
|
{
|
||||||
@@ -60,13 +59,9 @@ namespace Musify.Infrastructure.Messaging
|
|||||||
options.Password(password);
|
options.Password(password);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
busFactoryConfigurator.ReceiveEndpoint(ResizePictureConsumer.QueueName, endpointConfigurator =>
|
busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator =>
|
||||||
{
|
{
|
||||||
endpointConfigurator.ConfigureConsumer<ResizePictureConsumer>(busRegistrationContext);
|
endpointConfigurator.ConfigureConsumer<UpdatePlayListPictureConsumer>(busRegistrationContext);
|
||||||
} );
|
|
||||||
busFactoryConfigurator.ReceiveEndpoint(RemoveFileConsumer.QueueName, endpointConfigurator =>
|
|
||||||
{
|
|
||||||
endpointConfigurator.ConfigureConsumer<RemoveFileConsumer>(busRegistrationContext);
|
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ namespace Musify.Infrastructure.Pictures
|
|||||||
public class PictureHandler(ILogger<PictureHandler> logger)
|
public class PictureHandler(ILogger<PictureHandler> logger)
|
||||||
: IPictureHandler
|
: IPictureHandler
|
||||||
{
|
{
|
||||||
|
public string FileExtension => "webp";
|
||||||
|
|
||||||
|
public string ContentType => "image/webp";
|
||||||
|
|
||||||
public async Task<Result<Stream>> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken)
|
public async Task<Result<Stream>> ResizePictureAsync(Stream pictureStream, int width, int height, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -142,5 +142,31 @@ namespace Musify.Infrastructure.Storage
|
|||||||
|
|
||||||
return Result.Success(keyName);
|
return Result.Success(keyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Result> 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user