Changes
This commit is contained in:
@@ -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<Result> CheckQuotaAsync(
|
||||||
|
UploadIntentConfiguration config,
|
||||||
|
ILogger logger,
|
||||||
|
Guid userId,
|
||||||
|
long requiredBytes,
|
||||||
|
int requiredIntentCount,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<Result<UploadIntent>> ValidateAndLoadAsync(
|
||||||
|
UploadIntentConfiguration config,
|
||||||
|
Guid intentId,
|
||||||
|
Guid userId,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Ardalis.Result;
|
||||||
|
|
||||||
|
namespace Musify.Application.Extensions;
|
||||||
|
|
||||||
|
public static class ResultExtensions
|
||||||
|
{
|
||||||
|
public static Result<TOut> As<TIn, TOut>(this Result<TIn> 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<TOut>.NotFound(result.Errors.ToArray()),
|
||||||
|
ResultStatus.Forbidden => Result<TOut>.Forbidden(result.Errors.ToArray()),
|
||||||
|
ResultStatus.Unauthorized => Result<TOut>.Unauthorized(result.Errors.ToArray()),
|
||||||
|
ResultStatus.Invalid => Result<TOut>.Invalid(result.ValidationErrors.ToArray()),
|
||||||
|
ResultStatus.Conflict => Result<TOut>.Conflict(result.Errors.ToArray()),
|
||||||
|
ResultStatus.CriticalError => Result<TOut>.CriticalError(result.Errors.ToArray()),
|
||||||
|
ResultStatus.Unavailable => Result<TOut>.Unavailable(result.Errors.ToArray()),
|
||||||
|
ResultStatus.Error => Result<TOut>.Error(string.Join(";", result.Errors)),
|
||||||
|
_ => Result<TOut>.Error(string.Join(";", result.Errors))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ using Musify.Application.Events;
|
|||||||
using Musify.Application.PlayLists.Commands;
|
using Musify.Application.PlayLists.Commands;
|
||||||
using Musify.Application.PlayLists.Responses;
|
using Musify.Application.PlayLists.Responses;
|
||||||
using Musify.Application.UploadIntents;
|
using Musify.Application.UploadIntents;
|
||||||
|
using Musify.Application.Extensions;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Domain.Entities;
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
namespace Musify.Application.PlayLists.Handlers
|
namespace Musify.Application.PlayLists.Handlers
|
||||||
@@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
public class CreatePlayListCommandHandler(
|
public class CreatePlayListCommandHandler(
|
||||||
IEventBus eventBus,
|
IEventBus eventBus,
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
|
IUploadIntentService uploadIntentService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
ILogger<CreatePlayListCommandHandler> logger,
|
ILogger<CreatePlayListCommandHandler> logger,
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
ApplicationStorageConfiguration storageConfiguration,
|
||||||
@@ -38,13 +41,13 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
|
|
||||||
if (request.PictureIntentId.HasValue)
|
if (request.PictureIntentId.HasValue)
|
||||||
{
|
{
|
||||||
var validation = await UploadIntentHelpers.ValidateAndLoadAsync(
|
var validation = await uploadIntentService.ValidateAndLoadAsync(
|
||||||
database, storageService, uploadIntentConfiguration,
|
uploadIntentConfiguration,
|
||||||
request.PictureIntentId.Value, request.UserId, cancellationToken);
|
request.PictureIntentId.Value, request.UserId, cancellationToken);
|
||||||
if (!validation.IsSuccess)
|
if (!validation.IsSuccess)
|
||||||
return validation.Error!.Value;
|
return validation.As<UploadIntent, PlayListApplicationResponse>();
|
||||||
|
|
||||||
pictureIntent = validation.Intent!;
|
pictureIntent = validation.Value;
|
||||||
originalPictureName = pictureIntent.ObjectName;
|
originalPictureName = pictureIntent.ObjectName;
|
||||||
|
|
||||||
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName);
|
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, originalPictureName);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Mediator;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Application.Configuration;
|
using Musify.Application.Configuration;
|
||||||
using Musify.Application.PlayLists.Commands;
|
using Musify.Application.PlayLists.Commands;
|
||||||
using Musify.Application.PlayLists.Responses;
|
using Musify.Application.PlayLists.Responses;
|
||||||
@@ -13,6 +14,7 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
{
|
{
|
||||||
public class RequestPlayListPictureUploadCommandHandler(
|
public class RequestPlayListPictureUploadCommandHandler(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
|
IUploadIntentService uploadIntentService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
ILogger<RequestPlayListPictureUploadCommandHandler> logger,
|
ILogger<RequestPlayListPictureUploadCommandHandler> logger,
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
ApplicationStorageConfiguration storageConfiguration,
|
||||||
@@ -57,8 +59,8 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
await using var transaction = await database.BeginTransactionAsync(
|
await using var transaction = await database.BeginTransactionAsync(
|
||||||
System.Data.IsolationLevel.Serializable, cancellationToken);
|
System.Data.IsolationLevel.Serializable, cancellationToken);
|
||||||
|
|
||||||
var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync(
|
var quotaCheck = await uploadIntentService.CheckQuotaAsync(
|
||||||
database, uploadIntentConfiguration, logger,
|
uploadIntentConfiguration, logger,
|
||||||
request.UserId, effectiveSizeBytes, 1, cancellationToken);
|
request.UserId, effectiveSizeBytes, 1, cancellationToken);
|
||||||
if (!quotaCheck.IsSuccess)
|
if (!quotaCheck.IsSuccess)
|
||||||
return quotaCheck;
|
return quotaCheck;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using Musify.Application.Events;
|
|||||||
using Musify.Application.PlayLists.Commands;
|
using Musify.Application.PlayLists.Commands;
|
||||||
using Musify.Application.PlayLists.Responses;
|
using Musify.Application.PlayLists.Responses;
|
||||||
using Musify.Application.UploadIntents;
|
using Musify.Application.UploadIntents;
|
||||||
|
using Musify.Application.Extensions;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Domain.Entities;
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
namespace Musify.Application.PlayLists.Handlers
|
namespace Musify.Application.PlayLists.Handlers
|
||||||
@@ -15,6 +17,7 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
public class UpdatePlayListCommandHandler(
|
public class UpdatePlayListCommandHandler(
|
||||||
IEventBus eventBus,
|
IEventBus eventBus,
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
|
IUploadIntentService uploadIntentService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
ILogger<UpdatePlayListCommandHandler> logger,
|
ILogger<UpdatePlayListCommandHandler> logger,
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
ApplicationStorageConfiguration storageConfiguration,
|
||||||
@@ -50,13 +53,13 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
|
|
||||||
if (request.NewPictureIntentId.HasValue)
|
if (request.NewPictureIntentId.HasValue)
|
||||||
{
|
{
|
||||||
var validation = await UploadIntentHelpers.ValidateAndLoadAsync(
|
var validation = await uploadIntentService.ValidateAndLoadAsync(
|
||||||
database, storageService, uploadIntentConfiguration,
|
uploadIntentConfiguration,
|
||||||
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
|
request.NewPictureIntentId.Value, request.UserId, cancellationToken);
|
||||||
if (!validation.IsSuccess)
|
if (!validation.IsSuccess)
|
||||||
return validation.Error!.Value;
|
return validation.As<UploadIntent, PlayListApplicationResponse>();
|
||||||
|
|
||||||
var intent = validation.Intent!;
|
var intent = validation.Value;
|
||||||
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName);
|
var finalKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, intent.ObjectName);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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<Result> 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<Result<UploadIntent>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ using Musify.Application.Events;
|
|||||||
using Musify.Application.Tracks.Commands;
|
using Musify.Application.Tracks.Commands;
|
||||||
using Musify.Application.Tracks.Responses;
|
using Musify.Application.Tracks.Responses;
|
||||||
using Musify.Application.UploadIntents;
|
using Musify.Application.UploadIntents;
|
||||||
|
using Musify.Application.Extensions;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Domain.Entities;
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
namespace Musify.Application.Tracks.Handler
|
namespace Musify.Application.Tracks.Handler
|
||||||
@@ -15,6 +17,7 @@ namespace Musify.Application.Tracks.Handler
|
|||||||
public class CreateTrackCommandHandler(
|
public class CreateTrackCommandHandler(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
IEventBus eventBus,
|
IEventBus eventBus,
|
||||||
|
IUploadIntentService uploadIntentService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
ILogger<CreateTrackCommandHandler> logger,
|
ILogger<CreateTrackCommandHandler> logger,
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
ApplicationStorageConfiguration storageConfiguration,
|
||||||
@@ -32,20 +35,20 @@ namespace Musify.Application.Tracks.Handler
|
|||||||
return Result.NotFound($"User {request.UserId} not found");
|
return Result.NotFound($"User {request.UserId} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var pictureValidation = await UploadIntentHelpers.ValidateAndLoadAsync(
|
var pictureValidation = await uploadIntentService.ValidateAndLoadAsync(
|
||||||
database, storageService, uploadIntentConfiguration,
|
uploadIntentConfiguration,
|
||||||
request.PictureIntentId, request.UserId, cancellationToken);
|
request.PictureIntentId, request.UserId, cancellationToken);
|
||||||
if (!pictureValidation.IsSuccess)
|
if (!pictureValidation.IsSuccess)
|
||||||
return pictureValidation.Error!.Value;
|
return pictureValidation.As<UploadIntent, TrackApplicationResponse>();
|
||||||
|
|
||||||
var audioValidation = await UploadIntentHelpers.ValidateAndLoadAsync(
|
var audioValidation = await uploadIntentService.ValidateAndLoadAsync(
|
||||||
database, storageService, uploadIntentConfiguration,
|
uploadIntentConfiguration,
|
||||||
request.AudioIntentId, request.UserId, cancellationToken);
|
request.AudioIntentId, request.UserId, cancellationToken);
|
||||||
if (!audioValidation.IsSuccess)
|
if (!audioValidation.IsSuccess)
|
||||||
return audioValidation.Error!.Value;
|
return audioValidation.As<UploadIntent, TrackApplicationResponse>();
|
||||||
|
|
||||||
var pictureIntent = pictureValidation.Intent!;
|
var pictureIntent = pictureValidation.Value;
|
||||||
var audioIntent = audioValidation.Intent!;
|
var audioIntent = audioValidation.Value;
|
||||||
|
|
||||||
var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
var finalPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||||
var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName);
|
var finalAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(request.UserId, audioIntent.ObjectName);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ardalis.Result;
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
using Musify.Application.Configuration;
|
using Musify.Application.Configuration;
|
||||||
using Musify.Application.Tracks.Commands;
|
using Musify.Application.Tracks.Commands;
|
||||||
@@ -13,6 +14,7 @@ namespace Musify.Application.Tracks.Handler
|
|||||||
{
|
{
|
||||||
public class RequestTrackUploadUrlsCommandHandler(
|
public class RequestTrackUploadUrlsCommandHandler(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
|
IUploadIntentService uploadIntentService,
|
||||||
IStorageService storageService,
|
IStorageService storageService,
|
||||||
ILogger<RequestTrackUploadUrlsCommandHandler> logger,
|
ILogger<RequestTrackUploadUrlsCommandHandler> logger,
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
ApplicationStorageConfiguration storageConfiguration,
|
||||||
@@ -78,8 +80,8 @@ namespace Musify.Application.Tracks.Handler
|
|||||||
await using var transaction = await database.BeginTransactionAsync(
|
await using var transaction = await database.BeginTransactionAsync(
|
||||||
System.Data.IsolationLevel.Serializable, cancellationToken);
|
System.Data.IsolationLevel.Serializable, cancellationToken);
|
||||||
|
|
||||||
var quotaCheck = await UploadIntentHelpers.CheckQuotaAsync(
|
var quotaCheck = await uploadIntentService.CheckQuotaAsync(
|
||||||
database, uploadIntentConfiguration, logger,
|
uploadIntentConfiguration, logger,
|
||||||
request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken);
|
request.UserId, effectivePictureSize + effectiveAudioSize, 2, cancellationToken);
|
||||||
if (!quotaCheck.IsSuccess)
|
if (!quotaCheck.IsSuccess)
|
||||||
return quotaCheck;
|
return quotaCheck;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +1,9 @@
|
|||||||
using Ardalis.Result;
|
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
|
namespace Musify.Application.UploadIntents
|
||||||
{
|
{
|
||||||
internal static class UploadIntentHelpers
|
internal static class UploadIntentHelpers
|
||||||
{
|
{
|
||||||
internal static async Task<IntentValidationResult> 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<Result> 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<string> ValidateExtension(string fileType, string[] allowedExtensions)
|
internal static Result<string> ValidateExtension(string fileType, string[] allowedExtensions)
|
||||||
{
|
{
|
||||||
var extension = fileType.Trim();
|
var extension = fileType.Trim();
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Application.Abstractions.Application;
|
||||||
using Musify.Application.Configuration;
|
using Musify.Application.Configuration;
|
||||||
using Musify.Infrastructure.Configuration;
|
using Musify.Infrastructure.Configuration;
|
||||||
using Musify.Infrastructure.Jobs;
|
using Musify.Infrastructure.Jobs;
|
||||||
|
using Musify.Application.Services;
|
||||||
|
|
||||||
namespace Musify.Infrastructure.Services
|
namespace Musify.Infrastructure.Services
|
||||||
{
|
{
|
||||||
@@ -54,6 +56,8 @@ namespace Musify.Infrastructure.Services
|
|||||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||||
serviceProvider.GetRequiredService<IOptions<UploadIntentConfiguration>>().Value);
|
serviceProvider.GetRequiredService<IOptions<UploadIntentConfiguration>>().Value);
|
||||||
|
|
||||||
|
serviceDescriptors.AddScoped<IUploadIntentService, UploadIntentService>();
|
||||||
|
|
||||||
return serviceDescriptors;
|
return serviceDescriptors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user