From 348f5cb813756029c18f3b9c962f56ca3418854d Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 14 May 2026 21:42:31 +0200 Subject: [PATCH] Changes --- .../Application/IUploadIntentService.cs | 23 ++++++ Application/Extensions/ResultExtensions.cs | 25 +++++++ .../Handlers/CreatePlayListCommandHandler.cs | 13 ++-- ...uestPlayListPictureUploadCommandHandler.cs | 6 +- .../Handlers/UpdatePlayListCommandHandler.cs | 13 ++-- Application/Services/UploadIntentService.cs | 71 +++++++++++++++++++ .../Handler/CreateTrackCommandHandler.cs | 19 ++--- .../RequestTrackUploadUrlsCommandHandler.cs | 6 +- .../UploadIntents/IntentValidationResult.cs | 13 ---- .../UploadIntents/UploadIntentHelpers.cs | 65 ----------------- .../Services/ServicesDependencyInjection.cs | 4 ++ 11 files changed, 158 insertions(+), 100 deletions(-) create mode 100644 Application/Abstractions/Application/IUploadIntentService.cs create mode 100644 Application/Extensions/ResultExtensions.cs create mode 100644 Application/Services/UploadIntentService.cs delete mode 100644 Application/UploadIntents/IntentValidationResult.cs diff --git a/Application/Abstractions/Application/IUploadIntentService.cs b/Application/Abstractions/Application/IUploadIntentService.cs new file mode 100644 index 0000000..91c0bb7 --- /dev/null +++ b/Application/Abstractions/Application/IUploadIntentService.cs @@ -0,0 +1,23 @@ +using Ardalis.Result; +using Microsoft.Extensions.Logging; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Application.Abstractions.Application; + +public interface IUploadIntentService +{ + Task CheckQuotaAsync( + UploadIntentConfiguration config, + ILogger logger, + Guid userId, + long requiredBytes, + int requiredIntentCount, + CancellationToken cancellationToken); + + Task> ValidateAndLoadAsync( + UploadIntentConfiguration config, + Guid intentId, + Guid userId, + CancellationToken cancellationToken); +} diff --git a/Application/Extensions/ResultExtensions.cs b/Application/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..04bf866 --- /dev/null +++ b/Application/Extensions/ResultExtensions.cs @@ -0,0 +1,25 @@ +using Ardalis.Result; + +namespace Musify.Application.Extensions; + +public static class ResultExtensions +{ + public static Result As(this Result result) + { + return result.Status switch + { + ResultStatus.Ok or + ResultStatus.Created or + ResultStatus.NoContent => throw new InvalidOperationException($"Cannot convert a successful result ( {result.Status} ) to another type. Use Map or Bind instead."), + ResultStatus.NotFound => Result.NotFound(result.Errors.ToArray()), + ResultStatus.Forbidden => Result.Forbidden(result.Errors.ToArray()), + ResultStatus.Unauthorized => Result.Unauthorized(result.Errors.ToArray()), + ResultStatus.Invalid => Result.Invalid(result.ValidationErrors.ToArray()), + ResultStatus.Conflict => Result.Conflict(result.Errors.ToArray()), + ResultStatus.CriticalError => Result.CriticalError(result.Errors.ToArray()), + ResultStatus.Unavailable => Result.Unavailable(result.Errors.ToArray()), + ResultStatus.Error => Result.Error(string.Join(";", result.Errors)), + _ => Result.Error(string.Join(";", result.Errors)) + }; + } +} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs index 7ac68aa..930a718 100644 --- a/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class CreatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -38,13 +41,13 @@ namespace Musify.Application.PlayLists.Handlers if (request.PictureIntentId.HasValue) { - var validation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var validation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) - return validation.Error!.Value; + return validation.As(); - pictureIntent = validation.Intent!; + pictureIntent = validation.Value; originalPictureName = pictureIntent.ObjectName; var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName); @@ -131,4 +134,4 @@ namespace Musify.Application.PlayLists.Handlers } } } -} +} \ No newline at end of file diff --git a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs index a4ac2a1..ee6b51b 100644 --- a/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs +++ b/Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs @@ -3,6 +3,7 @@ using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; using Musify.Application.Configuration; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; @@ -13,6 +14,7 @@ namespace Musify.Application.PlayLists.Handlers { public class RequestPlayListPictureUploadCommandHandler( IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -57,8 +59,8 @@ namespace Musify.Application.PlayLists.Handlers await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( - database, uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentService.CheckQuotaAsync( + uploadIntentConfiguration, logger, request.UserId, effectiveSizeBytes, 1, cancellationToken); if (!quotaCheck.IsSuccess) return quotaCheck; diff --git a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs index fbbe76b..70b064a 100644 --- a/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.PlayLists.Commands; using Musify.Application.PlayLists.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.PlayLists.Handlers @@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers public class UpdatePlayListCommandHandler( IEventBus eventBus, IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -50,13 +53,13 @@ namespace Musify.Application.PlayLists.Handlers if (request.NewPictureIntentId.HasValue) { - var validation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var validation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.NewPictureIntentId.Value, request.UserId, cancellationToken); if (!validation.IsSuccess) - return validation.Error!.Value; + return validation.As(); - var intent = validation.Intent!; + var intent = validation.Value; var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName); try { @@ -121,4 +124,4 @@ namespace Musify.Application.PlayLists.Handlers } } } -} +} \ No newline at end of file diff --git a/Application/Services/UploadIntentService.cs b/Application/Services/UploadIntentService.cs new file mode 100644 index 0000000..f3c736f --- /dev/null +++ b/Application/Services/UploadIntentService.cs @@ -0,0 +1,71 @@ +using Ardalis.Result; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; +using Musify.Application.Configuration; +using Musify.Domain.Entities; + +namespace Musify.Application.Services; + +public sealed class UploadIntentService( + IDatabase database, + IStorageService storageService) : IUploadIntentService +{ + public async Task CheckQuotaAsync( + UploadIntentConfiguration config, + ILogger logger, + Guid userId, + long requiredBytes, + int requiredIntentCount, + CancellationToken cancellationToken) + { + var activeIntents = await database.UploadIntents + .AsNoTracking() + .Where(i => i.UserId == userId && i.Status == UploadIntentStatus.Issued) + .ToListAsync(cancellationToken); + + if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); + return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); + } + + var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); + if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) + { + logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); + return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); + } + + return Result.Success(); + } + + public async Task> ValidateAndLoadAsync( + UploadIntentConfiguration config, + Guid intentId, + Guid userId, + CancellationToken cancellationToken) + { + var intent = await database.UploadIntents + .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); + + if (intent is null || intent.UserId != userId) + return Result.NotFound("Upload intent not found or not accessible."); + + if (intent.Status == UploadIntentStatus.Consumed) + return Result.Conflict("Upload intent has already been consumed."); + + if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) + return Result.Invalid(new ValidationError("Upload intent has expired.")); + + var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); + if (metadata is null) + return Result.NotFound("Uploaded object not found in storage. Upload the file first."); + + if (metadata.ContentLength > config.MaxUploadBytes) + return Result.Invalid(new ValidationError($"Uploaded file exceeds the maximum allowed size of {config.MaxUploadBytes} bytes.")); + + return Result.Success(intent); + } +} diff --git a/Application/Tracks/Handler/CreateTrackCommandHandler.cs b/Application/Tracks/Handler/CreateTrackCommandHandler.cs index 1cfe978..7578ed3 100644 --- a/Application/Tracks/Handler/CreateTrackCommandHandler.cs +++ b/Application/Tracks/Handler/CreateTrackCommandHandler.cs @@ -8,6 +8,8 @@ using Musify.Application.Events; using Musify.Application.Tracks.Commands; using Musify.Application.Tracks.Responses; using Musify.Application.UploadIntents; +using Musify.Application.Extensions; +using Musify.Application.Abstractions.Application; using Musify.Domain.Entities; namespace Musify.Application.Tracks.Handler @@ -15,6 +17,7 @@ namespace Musify.Application.Tracks.Handler public class CreateTrackCommandHandler( IDatabase database, IEventBus eventBus, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -32,20 +35,20 @@ namespace Musify.Application.Tracks.Handler return Result.NotFound($"User {request.UserId} not found"); } - var pictureValidation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var pictureValidation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.PictureIntentId, request.UserId, cancellationToken); if (!pictureValidation.IsSuccess) - return pictureValidation.Error!.Value; + return pictureValidation.As(); - var audioValidation = await UploadIntentHelpers.ValidateAndLoadAsync( - database, storageService, uploadIntentConfiguration, + var audioValidation = await uploadIntentService.ValidateAndLoadAsync( + uploadIntentConfiguration, request.AudioIntentId, request.UserId, cancellationToken); if (!audioValidation.IsSuccess) - return audioValidation.Error!.Value; + return audioValidation.As(); - var pictureIntent = pictureValidation.Intent!; - var audioIntent = audioValidation.Intent!; + var pictureIntent = pictureValidation.Value; + var audioIntent = audioValidation.Value; var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName); var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName); diff --git a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs index 048cb66..d40fbf8 100644 --- a/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs +++ b/Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs @@ -2,6 +2,7 @@ using Ardalis.Result; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Musify.Application.Abstractions.Application; using Musify.Application.Abstractions.Infrastructure; using Musify.Application.Configuration; using Musify.Application.Tracks.Commands; @@ -13,6 +14,7 @@ namespace Musify.Application.Tracks.Handler { public class RequestTrackUploadUrlsCommandHandler( IDatabase database, + IUploadIntentService uploadIntentService, IStorageService storageService, ILogger logger, ApplicationStorageConfiguration storageConfiguration, @@ -78,8 +80,8 @@ namespace Musify.Application.Tracks.Handler await using var transaction = await database.BeginTransactionAsync( System.Data.IsolationLevel.Serializable, cancellationToken); - var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync( - database, uploadIntentConfiguration, logger, + var quotaCheck = await uploadIntentService.CheckQuotaAsync( + uploadIntentConfiguration, logger, request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken); if (!quotaCheck.IsSuccess) return quotaCheck; diff --git a/Application/UploadIntents/IntentValidationResult.cs b/Application/UploadIntents/IntentValidationResult.cs deleted file mode 100644 index 5271fc0..0000000 --- a/Application/UploadIntents/IntentValidationResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ardalis.Result; -using Musify.Domain.Entities; - -namespace Musify.Application.UploadIntents -{ - internal sealed record IntentValidationResult(Result? Error, UploadIntent? Intent) - { - public bool IsSuccess => Error is null; - - public static IntentValidationResult Ok(UploadIntent intent) => new(null, intent); - public static IntentValidationResult Fail(Result error) => new(error, null); - } -} diff --git a/Application/UploadIntents/UploadIntentHelpers.cs b/Application/UploadIntents/UploadIntentHelpers.cs index fdba870..8551a27 100644 --- a/Application/UploadIntents/UploadIntentHelpers.cs +++ b/Application/UploadIntents/UploadIntentHelpers.cs @@ -1,74 +1,9 @@ using Ardalis.Result; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Musify.Application.Abstractions.Infrastructure; -using Musify.Application.Configuration; -using Musify.Domain.Entities; namespace Musify.Application.UploadIntents { internal static class UploadIntentHelpers { - internal static async Task ValidateAndLoadAsync( - IDatabase database, - IStorageService storageService, - UploadIntentConfiguration config, - Guid intentId, - Guid userId, - CancellationToken cancellationToken) - { - var intent = await database.UploadIntents - .FirstOrDefaultAsync(i => i.Id == intentId, cancellationToken); - - if (intent is null || intent.UserId != userId) - return IntentValidationResult.Fail(Result.NotFound("Upload intent not found or not accessible.")); - - if (intent.Status == UploadIntentStatus.Consumed) - return IntentValidationResult.Fail(Result.Conflict("Upload intent has already been consumed.")); - - if (intent.Status == UploadIntentStatus.Expired || intent.ExpiresAt < DateTime.UtcNow) - return IntentValidationResult.Fail(Result.Invalid(new ValidationError("Upload intent has expired."))); - - var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken); - if (metadata is null) - return IntentValidationResult.Fail(Result.NotFound("Uploaded object not found in storage. Upload the file first.")); - - if (metadata.ContentLength > config.MaxUploadBytes) - return IntentValidationResult.Fail(Result.Invalid(new ValidationError($"Uploaded file exceeds the maximum allowed size of {config.MaxUploadBytes} bytes."))); - - return IntentValidationResult.Ok(intent); - } - - internal static async Task CheckQuotaAsync( - IDatabase database, - UploadIntentConfiguration config, - ILogger logger, - Guid userId, - long requiredBytes, - int requiredIntentCount, - CancellationToken cancellationToken) - { - var activeIntents = await database.UploadIntents - .AsNoTracking() - .Where(i => i.UserId == userId && i.Status == UploadIntentStatus.Issued) - .ToListAsync(cancellationToken); - - if (activeIntents.Count + requiredIntentCount > config.MaxActiveUploadIntentsPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload intents ({Max})", userId, config.MaxActiveUploadIntentsPerUser); - return Result.Invalid(new ValidationError("Upload intent limit exceeded. Wait for existing uploads to complete or expire.")); - } - - var activeBytes = activeIntents.Sum(i => i.ExpectedSizeBytes ?? config.DefaultExpectedPictureSizeBytes); - if (activeBytes + requiredBytes > config.MaxActiveUploadBytesPerUser) - { - logger.LogWarning("User {UserId} exceeded max active upload bytes ({Max})", userId, config.MaxActiveUploadBytesPerUser); - return Result.Invalid(new ValidationError("Upload byte quota exceeded. Wait for existing uploads to complete or expire.")); - } - - return Result.Success(); - } - internal static Result ValidateExtension(string fileType, string[] allowedExtensions) { var extension = fileType.Trim(); diff --git a/Infrastructure/Services/ServicesDependencyInjection.cs b/Infrastructure/Services/ServicesDependencyInjection.cs index 4e92451..47fa365 100644 --- a/Infrastructure/Services/ServicesDependencyInjection.cs +++ b/Infrastructure/Services/ServicesDependencyInjection.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Musify.Application.Abstractions.Infrastructure; +using Musify.Application.Abstractions.Application; using Musify.Application.Configuration; using Musify.Infrastructure.Configuration; using Musify.Infrastructure.Jobs; +using Musify.Application.Services; namespace Musify.Infrastructure.Services { @@ -54,6 +56,8 @@ namespace Musify.Infrastructure.Services serviceDescriptors.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); + serviceDescriptors.AddScoped(); + return serviceDescriptors; }