Fix query and API bugs: 403 for non-owners, page bounds, lifecycle filters, cover fallback, race-safe inserts, async album delete

This commit is contained in:
2026-09-24 05:14:05 +02:00
parent ab388e6ad6
commit 0e3576922f
90 changed files with 2083 additions and 322 deletions
@@ -0,0 +1,35 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.Albums;
internal class DeleteAlbumFromDbActivity(
IDatabase database,
ILogger<DeleteAlbumFromDbActivity> logger)
: IExecuteActivity<DeleteAlbumFromDbArguments>
{
public const string ExecuteEndpointName = "delete-album-from-db";
public async Task<ExecutionResult> Execute(ExecuteContext<DeleteAlbumFromDbArguments> executeContext)
{
var album = await database.Albums
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.AlbumId, executeContext.CancellationToken);
if (album == null)
{
logger.LogInformation(
"Album {AlbumId} not found while deleting from DB, skipping",
executeContext.Arguments.AlbumId);
return executeContext.Completed();
}
database.Albums.Remove(album);
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Deleted album {AlbumId} from DB", album.Id);
return executeContext.Completed();
}
}
@@ -0,0 +1,36 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.Activities.Albums;
internal class MarkAlbumAsRemovingActivity(
IDatabase database,
ILogger<MarkAlbumAsRemovingActivity> logger)
: IExecuteActivity<MarkAlbumAsRemovingArguments>
{
public const string ExecuteEndpointName = "mark-album-as-removing";
public async Task<ExecutionResult> Execute(ExecuteContext<MarkAlbumAsRemovingArguments> executeContext)
{
var album = await database.Albums
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.AlbumId, executeContext.CancellationToken);
if (album == null)
{
logger.LogInformation(
"Album {AlbumId} not found while marking as removing, skipping",
executeContext.Arguments.AlbumId);
return executeContext.Completed();
}
album.LifeCycleStatus = LifeCycleStatus.Removing;
await database.SaveChangesAsync(executeContext.CancellationToken);
logger.LogInformation("Marked album {AlbumId} as removing", album.Id);
return executeContext.Completed();
}
}
@@ -23,7 +23,8 @@ internal class DownloadFileFromBucketActivity(
using var fileStream = await storageHandler.GetFileAsync(
executeContext.Arguments.Bucket,
executeContext.Arguments.Key,
executeContext.CancellationToken);
executeContext.CancellationToken)
?? throw new FileNotFoundException($"Object {executeContext.Arguments.Bucket}/{executeContext.Arguments.Key} was not found.");
var destinationDirectory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(destinationDirectory))
@@ -0,0 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
internal record DeleteAlbumFromDbArguments(
Guid AlbumId);
@@ -0,0 +1,4 @@
namespace Musify.Infrastructure.MassTransit.Arguments;
internal record MarkAlbumAsRemovingArguments(
Guid AlbumId);
@@ -0,0 +1,24 @@
using MassTransit;
using Musify.Application.Events;
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
namespace Musify.Infrastructure.MassTransit.Consumers;
public class DeleteAlbumConsumer(
IBus bus,
DeleteAlbumRoutingSlipBuilder routingSlipBuilder)
: IConsumer<DeleteAlbumEvent>
{
public async Task Consume(ConsumeContext<DeleteAlbumEvent> consumeContext)
{
var routingSlip = (await routingSlipBuilder
.BuildAsync(
consumeContext.Message.AlbumId,
consumeContext.Message.UserId,
consumeContext.CorrelationId,
consumeContext.CancellationToken))
.Build();
await bus.Execute(routingSlip);
}
}
@@ -22,6 +22,7 @@ public static partial class MassTransitDependencyInjection
services.AddScoped<PictureWorkflowRoutingSlipBuilder>();
services.AddScoped<DeleteTrackRoutingSlipBuilder>();
services.AddScoped<DeletePlayListRoutingSlipBuilder>();
services.AddScoped<DeleteAlbumRoutingSlipBuilder>();
services.AddScoped<CreateTrackRoutingSlipBuilder>();
services.AddScoped<PlayListPictureSourceRoutingSlipBuilder>();
services.AddScoped<AlbumPictureSourceRoutingSlipBuilder>();
@@ -35,6 +36,7 @@ public static partial class MassTransitDependencyInjection
options.AddConsumer<UpdateTrackAudioConsumer>();
options.AddConsumer<DeleteTrackConsumer>();
options.AddConsumer<DeletePlayListConsumer>();
options.AddConsumer<DeleteAlbumConsumer>();
options.AddConsumer<CreateTrackConsumer>();
options.AddConsumer<CreatePlayListConsumer>();
options.AddConsumer<UpdatePlayListPictureSourceConsumer>();
@@ -58,6 +60,8 @@ public static partial class MassTransitDependencyInjection
options.AddExecuteActivity<MarkTrackAsFailedActivity, MarkTrackAsFailedArguments>();
options.AddExecuteActivity<MarkPlayListAsFailedActivity, MarkPlayListAsFailedArguments>();
options.AddExecuteActivity<MarkAlbumAsFailedActivity, MarkAlbumAsFailedArguments>();
options.AddExecuteActivity<MarkAlbumAsRemovingActivity, MarkAlbumAsRemovingArguments>();
options.AddExecuteActivity<DeleteAlbumFromDbActivity, DeleteAlbumFromDbArguments>();
options.AddExecuteActivity<PublishAlbumPictureProcessingEventActivity, PublishAlbumPictureProcessingEventArguments>();
options.AddActivity<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>();
@@ -3,6 +3,6 @@ namespace Musify.Infrastructure.MassTransit.Logs;
public record UpdateAlbumPictureLog(
Guid AlbumId,
string PreviousOriginalPictureKey,
string PreviousSmallPictureKey,
string PreviousMediumPictureKey,
string PreviousLargePictureKey);
string? PreviousSmallPictureKey,
string? PreviousMediumPictureKey,
string? PreviousLargePictureKey);
@@ -3,6 +3,6 @@ namespace Musify.Infrastructure.MassTransit.Logs;
public record UpdatePlayListPictureLog(
Guid PlayListId,
string PreviousOriginalPictureKey,
string PreviousSmallPictureKey,
string PreviousMediumPictureKey,
string PreviousLargePictureKey);
string? PreviousSmallPictureKey,
string? PreviousMediumPictureKey,
string? PreviousLargePictureKey);
@@ -9,6 +9,8 @@ internal static class ActivityNames
internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed";
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
internal const string MarkAlbumAsFailed = "MarkAlbumAsFailed";
internal const string MarkAlbumAsRemoving = "MarkAlbumAsRemoving";
internal const string DeleteAlbumFromDb = "DeleteAlbumFromDb";
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
@@ -55,4 +57,7 @@ internal static class ActivityNames
internal const string RemovePlayListLargePicture = "RemovePlayListLargePicture";
internal const string RemoveAlbumOriginalPicture = "RemoveAlbumOriginalPicture";
internal const string RemoveAlbumSmallPicture = "RemoveAlbumSmallPicture";
internal const string RemoveAlbumMediumPicture = "RemoveAlbumMediumPicture";
internal const string RemoveAlbumLargePicture = "RemoveAlbumLargePicture";
}
@@ -0,0 +1,93 @@
using MassTransit;
using MassTransit.Courier.Contracts;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Configuration;
using Musify.Application.Contracts;
using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Activities.Albums;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Consumers;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
public class DeleteAlbumRoutingSlipBuilder(
IDatabase database,
AlbumConfiguration albumConfiguration,
ApplicationStorageConfiguration storageConfiguration)
{
public async Task<RoutingSlipBuilder> BuildAsync(
Guid albumId,
long userId,
Guid? correlationId,
CancellationToken cancellationToken)
{
var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid());
routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
routingSlipBuilder.AddSubscription(
EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
var album = await database.Albums
.AsNoTracking()
.SingleOrDefaultAsync(p => p.Id == albumId, cancellationToken);
routingSlipBuilder.AddActivity(
ActivityNames.MarkAlbumAsRemoving,
EndpointHelper.BuildExecuteActivityUri(MarkAlbumAsRemovingActivity.ExecuteEndpointName),
new MarkAlbumAsRemovingArguments(albumId));
if (album?.Pictures != null)
{
var bucket = storageConfiguration.Bucket;
var routes = albumConfiguration.Routes;
if (album.Pictures.OriginalName is { } originalName)
{
routingSlipBuilder.AddActivity(
ActivityNames.RemoveAlbumOriginalPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments(
bucket,
routes.BuildOriginalPicturePath(userId, originalName)));
}
if (album.Pictures.SmallName is { } smallName)
{
routingSlipBuilder.AddActivity(
ActivityNames.RemoveAlbumSmallPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments(
bucket,
routes.BuildSmallPicturePath(smallName)));
}
if (album.Pictures.MediumName is { } mediumName)
{
routingSlipBuilder.AddActivity(
ActivityNames.RemoveAlbumMediumPicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments(
bucket,
routes.BuildMediumPicturePath(mediumName)));
}
if (album.Pictures.LargeName is { } largeName)
{
routingSlipBuilder.AddActivity(
ActivityNames.RemoveAlbumLargePicture,
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
new RemoveFileFromBucketArguments(
bucket,
routes.BuildLargePicturePath(largeName)));
}
}
routingSlipBuilder.AddActivity(
ActivityNames.DeleteAlbumFromDb,
EndpointHelper.BuildExecuteActivityUri(DeleteAlbumFromDbActivity.ExecuteEndpointName),
new DeleteAlbumFromDbArguments(albumId));
return routingSlipBuilder;
}
}