Update delete workflows
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Musify.Application.Events
|
||||||
|
{
|
||||||
|
public record DeletePlayListEvent(
|
||||||
|
Guid PlayListId,
|
||||||
|
Guid UserId);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Musify.Application.Events
|
|
||||||
{
|
|
||||||
public record RemoveFileEvent(
|
|
||||||
string Bucket,
|
|
||||||
string Key);
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,17 @@ using Ardalis.Result;
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Musify.Application.Configuration;
|
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
using Musify.Application.Events;
|
using Musify.Application.Events;
|
||||||
using Musify.Application.PlayLists.Commands;
|
using Musify.Application.PlayLists.Commands;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
namespace Musify.Application.PlayLists.Handlers
|
namespace Musify.Application.PlayLists.Handlers
|
||||||
{
|
{
|
||||||
public class DeletePlayListCommandHandler(IDatabase database, IEventBus eventBus,
|
public class DeletePlayListCommandHandler(
|
||||||
ApplicationStorageConfiguration storageConfiguration, PlayListConfiguration playListConfiguration, ILogger<DeletePlayListCommandHandler> logger)
|
IDatabase database,
|
||||||
|
IEventBus eventBus,
|
||||||
|
ILogger<DeletePlayListCommandHandler> logger)
|
||||||
: ICommandHandler<DeletePlayListCommand, Result>
|
: ICommandHandler<DeletePlayListCommand, Result>
|
||||||
{
|
{
|
||||||
public async ValueTask<Result> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
|
public async ValueTask<Result> Handle(DeletePlayListCommand request, CancellationToken cancellationToken)
|
||||||
@@ -28,101 +30,31 @@ namespace Musify.Application.PlayLists.Handlers
|
|||||||
return Result.Unauthorized();
|
return Result.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
var originalPictureName = playList.OriginalPictureName;
|
playList.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
var smallPictureName = playList.SmallPictureName;
|
|
||||||
var mediumPictureName = playList.MediumPictureName;
|
|
||||||
var largePictureName = playList.LargePictureName;
|
|
||||||
|
|
||||||
var originalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(playList.UserId, playList.OriginalPictureName);
|
try
|
||||||
var smallPictureKey = playListConfiguration.Routes.BuildSmallPicturePath(playList.SmallPictureName);
|
{
|
||||||
var mediumPictureKey = playListConfiguration.Routes.BuildMediumPicturePath(playList.MediumPictureName);
|
await eventBus.PublishAsync(
|
||||||
var largePictureKey = playListConfiguration.Routes.BuildLargePicturePath(playList.LargePictureName);
|
new DeletePlayListEvent(playList.Id, request.UserId),
|
||||||
|
|
||||||
var publishResult = await PublishRemoveFileEventsAsync(
|
|
||||||
playList.Id,
|
|
||||||
originalPictureName,
|
|
||||||
smallPictureName,
|
|
||||||
mediumPictureName,
|
|
||||||
largePictureName,
|
|
||||||
originalPictureKey,
|
|
||||||
smallPictureKey,
|
|
||||||
mediumPictureKey,
|
|
||||||
largePictureKey,
|
|
||||||
playListConfiguration.Routes,
|
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
}
|
||||||
if (!publishResult.IsSuccess)
|
catch (Exception exception)
|
||||||
return publishResult;
|
{
|
||||||
|
logger.LogError(exception, "Failed to publish delete playlist event for playlist {PlayListId}", request.PlayListId);
|
||||||
|
return Result.Error($"Failed to delete playlist {request.PlayListId}");
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
database.PlayLists.Remove(playList);
|
|
||||||
await database.SaveChangesAsync(cancellationToken);
|
await database.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
logger.LogError(exception, "Failed to delete playlist {PlayListId}", request.PlayListId);
|
logger.LogError(exception, "Failed to mark playlist {PlayListId} as removing", request.PlayListId);
|
||||||
return Result.Error($"Failed to delete playlist {request.PlayListId}");
|
return Result.Error($"Failed to delete playlist {request.PlayListId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.NoContent();
|
return Result.NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<Result> PublishRemoveFileEventsAsync(
|
|
||||||
Guid playListId,
|
|
||||||
string originalPictureName,
|
|
||||||
string smallPictureName,
|
|
||||||
string mediumPictureName,
|
|
||||||
string largePictureName,
|
|
||||||
string originalPictureKey,
|
|
||||||
string smallPictureKey,
|
|
||||||
string mediumPictureKey,
|
|
||||||
string largePictureKey,
|
|
||||||
PlayListRoutes playListRoutes,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (originalPictureName != playListRoutes.PresetOriginalPicture)
|
|
||||||
{
|
|
||||||
var result = await PublishRemoveFileEventAsync(playListId, originalPictureKey, cancellationToken);
|
|
||||||
if (!result.IsSuccess)
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (smallPictureName != playListRoutes.PresetSmallPicture)
|
|
||||||
{
|
|
||||||
var result = await PublishRemoveFileEventAsync(playListId, smallPictureKey, cancellationToken);
|
|
||||||
if (!result.IsSuccess)
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (mediumPictureName != playListRoutes.PresetMediumPicture)
|
|
||||||
{
|
|
||||||
var result = await PublishRemoveFileEventAsync(playListId, mediumPictureKey, cancellationToken);
|
|
||||||
if (!result.IsSuccess)
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (largePictureName != playListRoutes.PresetLargePicture)
|
|
||||||
{
|
|
||||||
var result = await PublishRemoveFileEventAsync(playListId, largePictureKey, cancellationToken);
|
|
||||||
if (!result.IsSuccess)
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.Success();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Result> PublishRemoveFileEventAsync(Guid playListId, string key, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await eventBus.PublishAsync(new RemoveFileEvent(
|
|
||||||
storageConfiguration.Bucket,
|
|
||||||
key), cancellationToken);
|
|
||||||
return Result.Success();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(exception, "Failed to publish remove file event for playlist {PlayListId} with key {Key}", playListId, key);
|
|
||||||
return Result.Error($"Failed to publish remove file event for playlist {playListId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,8 +40,7 @@ namespace Musify.Application.Tracks.Handler
|
|||||||
return Result.Conflict("Track is currently being processed and cannot be deleted");
|
return Result.Conflict("Track is currently being processed and cannot be deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
track.PicturesProcessingStatus = ProcessingStatus.Processing;
|
track.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Musify.Domain.Entities
|
||||||
|
{
|
||||||
|
public enum LifeCycleStatus
|
||||||
|
{
|
||||||
|
Active = 0,
|
||||||
|
Removing = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,9 @@ namespace Musify.Domain.Entities
|
|||||||
[MaxLength(64)]
|
[MaxLength(64)]
|
||||||
public required string LargePictureName { get; set; }
|
public required string LargePictureName { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ namespace Musify.Domain.Entities
|
|||||||
[Required]
|
[Required]
|
||||||
public ProcessingStatus AudioTranscodeProcessingStatus { get; set; } = ProcessingStatus.Pending;
|
public ProcessingStatus AudioTranscodeProcessingStatus { get; set; } = ProcessingStatus.Pending;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public int RetryCount { get; set; } = 0;
|
public int RetryCount { get; set; } = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using MassTransit;
|
using MassTransit;
|
||||||
|
using Amazon.S3;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.Activities.Files
|
namespace Musify.Infrastructure.MassTransit.Activities.Files
|
||||||
{
|
{
|
||||||
@@ -22,6 +24,16 @@ namespace Musify.Infrastructure.MassTransit.Activities.Files
|
|||||||
executeContext.CancellationToken);
|
executeContext.CancellationToken);
|
||||||
return executeContext.Completed();
|
return executeContext.Completed();
|
||||||
}
|
}
|
||||||
|
catch (AmazonS3Exception exception) when (
|
||||||
|
exception.StatusCode == HttpStatusCode.NotFound
|
||||||
|
|| string.Equals(exception.ErrorCode, "NoSuchKey", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Key {Key} not found in bucket {Bucket}, skipping",
|
||||||
|
executeContext.Arguments.Key,
|
||||||
|
executeContext.Arguments.Bucket);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
logger.LogError(exception, "Failed to remove {Key} from bucket {Bucket}",
|
logger.LogError(exception, "Failed to remove {Key} from bucket {Bucket}",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
|
||||||
|
{
|
||||||
|
internal class DeletePlayListFromDbActivity(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<DeletePlayListFromDbActivity> logger)
|
||||||
|
: IExecuteActivity<DeletePlayListFromDbArguments>
|
||||||
|
{
|
||||||
|
public const string ExecuteEndpointName = "delete-playList-from-db";
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<DeletePlayListFromDbArguments> executeContext)
|
||||||
|
{
|
||||||
|
var playList = await database.PlayLists
|
||||||
|
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken);
|
||||||
|
|
||||||
|
if (playList is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"PlayList {PlayListId} not found while deleting from DB, skipping",
|
||||||
|
executeContext.Arguments.PlayListId);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
database.PlayLists.Remove(playList);
|
||||||
|
await database.SaveChangesAsync(executeContext.CancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Deleted playlist {PlayListId} from DB", playList.Id);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
|
||||||
|
{
|
||||||
|
internal record DeletePlayListFromDbArguments(
|
||||||
|
Guid PlayListId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
|
||||||
|
{
|
||||||
|
internal class MarkPlayListAsRemovingActivity(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<MarkPlayListAsRemovingActivity> logger)
|
||||||
|
: IExecuteActivity<MarkPlayListAsRemovingArguments>
|
||||||
|
{
|
||||||
|
public const string ExecuteEndpointName = "mark-playList-as-removing";
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<MarkPlayListAsRemovingArguments> executeContext)
|
||||||
|
{
|
||||||
|
var playList = await database.PlayLists
|
||||||
|
.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, executeContext.CancellationToken);
|
||||||
|
|
||||||
|
if (playList is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"PlayList {PlayListId} not found while marking as removing, skipping",
|
||||||
|
executeContext.Arguments.PlayListId);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
playList.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
|
await database.SaveChangesAsync(executeContext.CancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Marked playlist {PlayListId} as removing", playList.Id);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.PlayLists
|
||||||
|
{
|
||||||
|
internal record MarkPlayListAsRemovingArguments(
|
||||||
|
Guid PlayListId);
|
||||||
|
}
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
using MassTransit;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Musify.Application.Configuration;
|
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
|
||||||
using Musify.Domain.Entities;
|
|
||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
|
||||||
{
|
|
||||||
internal class DeleteTrackActivity(
|
|
||||||
IDatabase database,
|
|
||||||
IStorageService storageService,
|
|
||||||
TrackConfiguration trackConfiguration,
|
|
||||||
ApplicationStorageConfiguration storageConfiguration,
|
|
||||||
ILogger<DeleteTrackActivity> logger)
|
|
||||||
: IExecuteActivity<DeleteTrackArguments>
|
|
||||||
{
|
|
||||||
public const string ExecuteEndpointName = "delete-track";
|
|
||||||
|
|
||||||
public async Task<ExecutionResult> Execute(ExecuteContext<DeleteTrackArguments> executeContext)
|
|
||||||
{
|
|
||||||
Track track;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var getTrack = await database.Tracks.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
|
|
||||||
track = getTrack ?? throw new InvalidOperationException($"Track with id {executeContext.Arguments.TrackId} not found");
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(exception, "Failed to retrieve track {TrackId} for deletion", executeContext.Arguments.TrackId);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RemovePicturesAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken);
|
|
||||||
track.PicturesProcessingStatus = ProcessingStatus.Completed;
|
|
||||||
await database.SaveChangesAsync(executeContext.CancellationToken);
|
|
||||||
|
|
||||||
await RemoveAudiosAsync(track, executeContext.Arguments.UserId, executeContext.CancellationToken);
|
|
||||||
track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed;
|
|
||||||
await database.SaveChangesAsync(executeContext.CancellationToken);
|
|
||||||
|
|
||||||
database.Tracks.Remove(track);
|
|
||||||
await database.SaveChangesAsync(executeContext.CancellationToken);
|
|
||||||
|
|
||||||
logger.LogInformation("Deleted track {TrackId}", track.Id);
|
|
||||||
return executeContext.Completed();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(exception, "Failed to delete track {TrackId}", executeContext.Arguments.TrackId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
track.PicturesProcessingStatus = ProcessingStatus.Failed;
|
|
||||||
track.AudioTranscodeProcessingStatus = ProcessingStatus.Failed;
|
|
||||||
await database.SaveChangesAsync(executeContext.CancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception dbException)
|
|
||||||
{
|
|
||||||
logger.LogError(dbException, "Failed to update processing statuses to Failed for track {TrackId}", executeContext.Arguments.TrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RemovePicturesAsync(Track track, Guid userId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName), cancellationToken);
|
|
||||||
|
|
||||||
if (!track.IsPicturesProcessed)
|
|
||||||
{
|
|
||||||
logger.LogInformation(
|
|
||||||
"Track {TrackId} pictures are not fully processed, skipping removal of small, medium, and large pictures",
|
|
||||||
track.Id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName), cancellationToken);
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName), cancellationToken);
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RemoveAudiosAsync(Track track, Guid userId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName), cancellationToken);
|
|
||||||
|
|
||||||
if (!track.IsAudioProcessed)
|
|
||||||
{
|
|
||||||
logger.LogInformation(
|
|
||||||
"Track {TrackId} audio is not fully processed, skipping removal of processed audio",
|
|
||||||
track.Id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await RemoveFileAsync(trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RemoveFileAsync(string key, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
logger.LogDebug("Removing file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket);
|
|
||||||
await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(exception, "Failed to remove file {Key} from bucket {Bucket}", key, storageConfiguration.Bucket);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
|
||||||
{
|
|
||||||
internal record DeleteTrackArguments(
|
|
||||||
Guid TrackId,
|
|
||||||
Guid UserId);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
||||||
|
{
|
||||||
|
internal class DeleteTrackFromDbActivity(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<DeleteTrackFromDbActivity> logger)
|
||||||
|
: IExecuteActivity<DeleteTrackFromDbArguments>
|
||||||
|
{
|
||||||
|
public const string ExecuteEndpointName = "delete-track-from-db";
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<DeleteTrackFromDbArguments> executeContext)
|
||||||
|
{
|
||||||
|
var track = await database.Tracks
|
||||||
|
.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
|
||||||
|
|
||||||
|
if (track is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Track {TrackId} not found while deleting from DB, skipping",
|
||||||
|
executeContext.Arguments.TrackId);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
database.Tracks.Remove(track);
|
||||||
|
await database.SaveChangesAsync(executeContext.CancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Deleted track {TrackId} from DB", track.Id);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
||||||
|
{
|
||||||
|
internal record DeleteTrackFromDbArguments(
|
||||||
|
Guid TrackId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
||||||
|
{
|
||||||
|
internal class MarkTrackAsRemovingActivity(
|
||||||
|
IDatabase database,
|
||||||
|
ILogger<MarkTrackAsRemovingActivity> logger)
|
||||||
|
: IExecuteActivity<MarkTrackAsRemovingArguments>
|
||||||
|
{
|
||||||
|
public const string ExecuteEndpointName = "mark-track-as-removing";
|
||||||
|
|
||||||
|
public async Task<ExecutionResult> Execute(ExecuteContext<MarkTrackAsRemovingArguments> executeContext)
|
||||||
|
{
|
||||||
|
var track = await database.Tracks
|
||||||
|
.SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken);
|
||||||
|
|
||||||
|
if (track is null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Track {TrackId} not found while marking as removing, skipping",
|
||||||
|
executeContext.Arguments.TrackId);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
|
||||||
|
track.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
|
await database.SaveChangesAsync(executeContext.CancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation("Marked track {TrackId} as removing", track.Id);
|
||||||
|
return executeContext.Completed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Musify.Infrastructure.MassTransit.Activities.Tracks
|
||||||
|
{
|
||||||
|
internal record MarkTrackAsRemovingArguments(
|
||||||
|
Guid TrackId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using Musify.Application.Events;
|
||||||
|
using Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.Consumers
|
||||||
|
{
|
||||||
|
public class DeletePlayListConsumer(
|
||||||
|
IBus bus,
|
||||||
|
DeletePlayListRoutingSlipBuilder routingSlipBuilder)
|
||||||
|
: IConsumer<DeletePlayListEvent>
|
||||||
|
{
|
||||||
|
public const string QueueName = "delete-playList";
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<DeletePlayListEvent> consumeContext)
|
||||||
|
{
|
||||||
|
var routingSlip = (await routingSlipBuilder
|
||||||
|
.BuildAsync(
|
||||||
|
consumeContext.Message.PlayListId,
|
||||||
|
consumeContext.Message.UserId,
|
||||||
|
consumeContext.CorrelationId,
|
||||||
|
consumeContext.CancellationToken))
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await bus.Execute(routingSlip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@ namespace Musify.Infrastructure.MassTransit.Consumers
|
|||||||
|
|
||||||
public async Task Consume(ConsumeContext<DeleteTrackEvent> consumeContext)
|
public async Task Consume(ConsumeContext<DeleteTrackEvent> consumeContext)
|
||||||
{
|
{
|
||||||
var routingSlip = routingSlipBuilder
|
var routingSlip = (await routingSlipBuilder
|
||||||
.Build(consumeContext.Message.TrackId, consumeContext.Message.UserId, consumeContext.CorrelationId)
|
.BuildAsync(consumeContext.Message.TrackId, consumeContext.Message.UserId, consumeContext.CorrelationId, consumeContext.CancellationToken))
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
await bus.Execute(routingSlip);
|
await bus.Execute(routingSlip);
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
using MassTransit;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Musify.Application.Abstractions.Infrastructure;
|
|
||||||
using Musify.Application.Events;
|
|
||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.Consumers
|
|
||||||
{
|
|
||||||
public class RemoveFileConsumer(
|
|
||||||
IStorageService storageService,
|
|
||||||
ILogger<RemoveFileConsumer> logger)
|
|
||||||
: IConsumer<RemoveFileEvent>
|
|
||||||
{
|
|
||||||
public const string QueueName = "remove-file";
|
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<RemoveFileEvent> consumeContext)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await storageService.RemoveFileAsync(
|
|
||||||
consumeContext.Message.Bucket,
|
|
||||||
consumeContext.Message.Key,
|
|
||||||
consumeContext.CancellationToken);
|
|
||||||
|
|
||||||
logger.LogInformation(
|
|
||||||
"Removed file {Key} from bucket {Bucket}",
|
|
||||||
consumeContext.Message.Key,
|
|
||||||
consumeContext.Message.Bucket);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(
|
|
||||||
exception,
|
|
||||||
"Failed to remove file {Key} from bucket {Bucket}",
|
|
||||||
consumeContext.Message.Key,
|
|
||||||
consumeContext.Message.Bucket);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+29
-4
@@ -6,6 +6,7 @@ using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
|||||||
using Musify.Infrastructure.MassTransit.Activities.Audio;
|
using Musify.Infrastructure.MassTransit.Activities.Audio;
|
||||||
using Musify.Infrastructure.MassTransit.Activities.Files;
|
using Musify.Infrastructure.MassTransit.Activities.Files;
|
||||||
using Musify.Infrastructure.MassTransit.Activities.Logs;
|
using Musify.Infrastructure.MassTransit.Activities.Logs;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.PlayLists;
|
||||||
using Musify.Infrastructure.MassTransit.Activities.Tracks;
|
using Musify.Infrastructure.MassTransit.Activities.Tracks;
|
||||||
using Musify.Infrastructure.MassTransit.Arguments;
|
using Musify.Infrastructure.MassTransit.Arguments;
|
||||||
using Musify.Infrastructure.MassTransit.Consumers;
|
using Musify.Infrastructure.MassTransit.Consumers;
|
||||||
@@ -21,6 +22,7 @@ namespace Musify.Infrastructure.MassTransit
|
|||||||
services.AddScoped<AudioWorkflowRoutingSlipBuilder>();
|
services.AddScoped<AudioWorkflowRoutingSlipBuilder>();
|
||||||
services.AddScoped<PictureWorkflowRoutingSlipBuilder>();
|
services.AddScoped<PictureWorkflowRoutingSlipBuilder>();
|
||||||
services.AddScoped<DeleteTrackRoutingSlipBuilder>();
|
services.AddScoped<DeleteTrackRoutingSlipBuilder>();
|
||||||
|
services.AddScoped<DeletePlayListRoutingSlipBuilder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options)
|
static void RegisterConsumersAndActivities(IBusRegistrationConfigurator options)
|
||||||
@@ -30,12 +32,15 @@ namespace Musify.Infrastructure.MassTransit
|
|||||||
options.AddConsumer<RoutingSlipCleanUpConsumer>();
|
options.AddConsumer<RoutingSlipCleanUpConsumer>();
|
||||||
options.AddConsumer<UpdateTrackAudioConsumer>();
|
options.AddConsumer<UpdateTrackAudioConsumer>();
|
||||||
options.AddConsumer<DeleteTrackConsumer>();
|
options.AddConsumer<DeleteTrackConsumer>();
|
||||||
options.AddConsumer<RemoveFileConsumer>();
|
options.AddConsumer<DeletePlayListConsumer>();
|
||||||
|
|
||||||
options.AddExecuteActivity<RemoveFileFromBucketActivity, RemoveFileFromBucketArguments>();
|
options.AddExecuteActivity<RemoveFileFromBucketActivity, RemoveFileFromBucketArguments>();
|
||||||
options.AddExecuteActivity<GenerateAudioWorkflowPathsActivity, GenerateAudioWorkflowPathsArguments>();
|
options.AddExecuteActivity<GenerateAudioWorkflowPathsActivity, GenerateAudioWorkflowPathsArguments>();
|
||||||
options.AddExecuteActivity<GeneratePictureWorkflowPathsActivity, GeneratePictureWorkflowPathsArguments>();
|
options.AddExecuteActivity<GeneratePictureWorkflowPathsActivity, GeneratePictureWorkflowPathsArguments>();
|
||||||
options.AddExecuteActivity<DeleteTrackActivity, DeleteTrackArguments>();
|
options.AddExecuteActivity<MarkTrackAsRemovingActivity, MarkTrackAsRemovingArguments>();
|
||||||
|
options.AddExecuteActivity<DeleteTrackFromDbActivity, DeleteTrackFromDbArguments>();
|
||||||
|
options.AddExecuteActivity<MarkPlayListAsRemovingActivity, MarkPlayListAsRemovingArguments>();
|
||||||
|
options.AddExecuteActivity<DeletePlayListFromDbActivity, DeletePlayListFromDbArguments>();
|
||||||
|
|
||||||
options.AddActivity<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>();
|
options.AddActivity<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>();
|
||||||
options.AddActivity<UpdatePlayListPictureActivity, UpdatePlayListPictureArguments, UpdatePlayListPictureLog>();
|
options.AddActivity<UpdatePlayListPictureActivity, UpdatePlayListPictureArguments, UpdatePlayListPictureLog>();
|
||||||
@@ -75,10 +80,10 @@ namespace Musify.Infrastructure.MassTransit
|
|||||||
busRegistrationContext,
|
busRegistrationContext,
|
||||||
DeleteTrackConsumer.QueueName);
|
DeleteTrackConsumer.QueueName);
|
||||||
|
|
||||||
ConfigureConsumerEndpoint<RemoveFileConsumer>(
|
ConfigureConsumerEndpoint<DeletePlayListConsumer>(
|
||||||
busFactoryConfigurator,
|
busFactoryConfigurator,
|
||||||
busRegistrationContext,
|
busRegistrationContext,
|
||||||
RemoveFileConsumer.QueueName);
|
DeletePlayListConsumer.QueueName);
|
||||||
|
|
||||||
ConfigureConsumerEndpoint<RoutingSlipCleanUpConsumer>(
|
ConfigureConsumerEndpoint<RoutingSlipCleanUpConsumer>(
|
||||||
busFactoryConfigurator,
|
busFactoryConfigurator,
|
||||||
@@ -100,6 +105,26 @@ namespace Musify.Infrastructure.MassTransit
|
|||||||
busRegistrationContext,
|
busRegistrationContext,
|
||||||
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName);
|
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName);
|
||||||
|
|
||||||
|
ConfigureExecuteActivityEndpoint<MarkTrackAsRemovingActivity, MarkTrackAsRemovingArguments>(
|
||||||
|
busFactoryConfigurator,
|
||||||
|
busRegistrationContext,
|
||||||
|
MarkTrackAsRemovingActivity.ExecuteEndpointName);
|
||||||
|
|
||||||
|
ConfigureExecuteActivityEndpoint<DeleteTrackFromDbActivity, DeleteTrackFromDbArguments>(
|
||||||
|
busFactoryConfigurator,
|
||||||
|
busRegistrationContext,
|
||||||
|
DeleteTrackFromDbActivity.ExecuteEndpointName);
|
||||||
|
|
||||||
|
ConfigureExecuteActivityEndpoint<MarkPlayListAsRemovingActivity, MarkPlayListAsRemovingArguments>(
|
||||||
|
busFactoryConfigurator,
|
||||||
|
busRegistrationContext,
|
||||||
|
MarkPlayListAsRemovingActivity.ExecuteEndpointName);
|
||||||
|
|
||||||
|
ConfigureExecuteActivityEndpoint<DeletePlayListFromDbActivity, DeletePlayListFromDbArguments>(
|
||||||
|
busFactoryConfigurator,
|
||||||
|
busRegistrationContext,
|
||||||
|
DeletePlayListFromDbActivity.ExecuteEndpointName);
|
||||||
|
|
||||||
ConfigureActivityEndpoint<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>(
|
ConfigureActivityEndpoint<ResizePictureActivity, ResizePictureLocalArguments, ResizePictureLog>(
|
||||||
busFactoryConfigurator,
|
busFactoryConfigurator,
|
||||||
busRegistrationContext,
|
busRegistrationContext,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip
|
|||||||
internal static class ActivityNames
|
internal static class ActivityNames
|
||||||
{
|
{
|
||||||
internal const string DeleteTrack = "DeleteTrack";
|
internal const string DeleteTrack = "DeleteTrack";
|
||||||
|
internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving";
|
||||||
|
internal const string DeleteTrackFromDb = "DeleteTrackFromDb";
|
||||||
|
internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving";
|
||||||
|
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
|
||||||
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
|
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
|
||||||
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
|
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
|
||||||
internal const string DownloadFile = "DownloadFile";
|
internal const string DownloadFile = "DownloadFile";
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using MassTransit;
|
||||||
|
using MassTransit.Courier.Contracts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Application.Configuration;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.Files;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.PlayLists;
|
||||||
|
using Musify.Infrastructure.MassTransit.Consumers;
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
||||||
|
{
|
||||||
|
public class DeletePlayListRoutingSlipBuilder(
|
||||||
|
IDatabase database,
|
||||||
|
PlayListConfiguration playListConfiguration,
|
||||||
|
ApplicationStorageConfiguration storageConfiguration)
|
||||||
|
{
|
||||||
|
public async Task<RoutingSlipBuilder> BuildAsync(
|
||||||
|
Guid playListId,
|
||||||
|
Guid 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 playList = await database.PlayLists
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(p => p.Id == playListId, cancellationToken);
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
ActivityNames.MarkPlayListAsRemoving,
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(MarkPlayListAsRemovingActivity.ExecuteEndpointName),
|
||||||
|
new MarkPlayListAsRemovingArguments(playListId));
|
||||||
|
|
||||||
|
if (playList is not null)
|
||||||
|
{
|
||||||
|
var bucket = storageConfiguration.Bucket;
|
||||||
|
var routes = playListConfiguration.Routes;
|
||||||
|
|
||||||
|
if (playList.OriginalPictureName != routes.PresetOriginalPicture)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemovePlayListOriginalPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
routes.BuildOriginalPicturePath(userId, playList.OriginalPictureName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playList.SmallPictureName != routes.PresetSmallPicture)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemovePlayListSmallPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
routes.BuildSmallPicturePath(playList.SmallPictureName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playList.MediumPictureName != routes.PresetMediumPicture)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemovePlayListMediumPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
routes.BuildMediumPicturePath(playList.MediumPictureName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playList.LargePictureName != routes.PresetLargePicture)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemovePlayListLargePicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
routes.BuildLargePicturePath(playList.LargePictureName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
ActivityNames.DeletePlayListFromDb,
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(DeletePlayListFromDbActivity.ExecuteEndpointName),
|
||||||
|
new DeletePlayListFromDbArguments(playListId));
|
||||||
|
|
||||||
|
return routingSlipBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
using MassTransit;
|
using MassTransit;
|
||||||
using MassTransit.Courier.Contracts;
|
using MassTransit.Courier.Contracts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Musify.Application.Abstractions.Infrastructure;
|
||||||
|
using Musify.Application.Configuration;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.Files;
|
||||||
|
using Musify.Infrastructure.MassTransit.Activities.Arguments;
|
||||||
using Musify.Infrastructure.MassTransit.Activities.Tracks;
|
using Musify.Infrastructure.MassTransit.Activities.Tracks;
|
||||||
using Musify.Infrastructure.MassTransit.Consumers;
|
using Musify.Infrastructure.MassTransit.Consumers;
|
||||||
|
|
||||||
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
||||||
{
|
{
|
||||||
public class DeleteTrackRoutingSlipBuilder
|
public class DeleteTrackRoutingSlipBuilder(
|
||||||
|
IDatabase database,
|
||||||
|
TrackConfiguration trackConfiguration,
|
||||||
|
ApplicationStorageConfiguration storageConfiguration)
|
||||||
{
|
{
|
||||||
public RoutingSlipBuilder Build(Guid trackId, Guid userId, Guid? correlationId)
|
public async Task<RoutingSlipBuilder> BuildAsync(
|
||||||
|
Guid trackId,
|
||||||
|
Guid userId,
|
||||||
|
Guid? correlationId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid());
|
var routingSlipBuilder = new RoutingSlipBuilder(NewId.NextGuid());
|
||||||
routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
|
routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
|
||||||
@@ -16,10 +28,72 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
|
|||||||
EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
|
EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
|
||||||
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
|
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
|
||||||
|
|
||||||
|
var track = await database.Tracks
|
||||||
|
.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
|
||||||
|
|
||||||
routingSlipBuilder.AddActivity(
|
routingSlipBuilder.AddActivity(
|
||||||
ActivityNames.DeleteTrack,
|
ActivityNames.MarkTrackAsRemoving,
|
||||||
EndpointHelper.BuildExecuteActivityUri(DeleteTrackActivity.ExecuteEndpointName),
|
EndpointHelper.BuildExecuteActivityUri(MarkTrackAsRemovingActivity.ExecuteEndpointName),
|
||||||
new DeleteTrackArguments(trackId, userId));
|
new MarkTrackAsRemovingArguments(trackId));
|
||||||
|
|
||||||
|
if (track is not null)
|
||||||
|
{
|
||||||
|
var bucket = storageConfiguration.Bucket;
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackOriginalPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildOriginalPicturePath(userId, track.OriginalPictureName)));
|
||||||
|
|
||||||
|
if (track.IsPicturesProcessed)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackSmallPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildSmallPicturePath(track.SmallPictureName)));
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackMediumPicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildMediumPicturePath(track.MediumPictureName)));
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackLargePicture",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildLargePicturePath(track.LargePictureName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackOriginalAudio",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildOriginalAudioPath(userId, track.OriginalAudioName)));
|
||||||
|
|
||||||
|
if (track.IsAudioProcessed)
|
||||||
|
{
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
"RemoveTrackProcessedAudio",
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName),
|
||||||
|
new RemoveFileFromBucketArguments(
|
||||||
|
bucket,
|
||||||
|
trackConfiguration.Routes.BuildProcessedAudioPath(track.AudioFolderName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routingSlipBuilder.AddActivity(
|
||||||
|
ActivityNames.DeleteTrackFromDb,
|
||||||
|
EndpointHelper.BuildExecuteActivityUri(DeleteTrackFromDbActivity.ExecuteEndpointName),
|
||||||
|
new DeleteTrackFromDbArguments(trackId));
|
||||||
|
|
||||||
return routingSlipBuilder;
|
return routingSlipBuilder;
|
||||||
}
|
}
|
||||||
|
|||||||
+518
@@ -0,0 +1,518 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Musify.Infrastructure.Persistence;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(Database))]
|
||||||
|
[Migration("20260514161518_AddLifeCycleStatus")]
|
||||||
|
partial class AddLifeCycleStatus
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.5")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Consumed")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("ConsumerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Delivered")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpirationTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("LastSequenceNumber")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid>("LockId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MessageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("ReceiveCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Received")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<byte[]>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("bytea");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Delivered");
|
||||||
|
|
||||||
|
b.ToTable("InboxState");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("SequenceNumber")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
|
||||||
|
|
||||||
|
b.Property<string>("Body")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ConversationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CorrelationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("DestinationAddress")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("EnqueueTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpirationTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FaultAddress")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Headers")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("InboxConsumerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("InboxMessageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("InitiatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MessageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("MessageType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OutboxId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Properties")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RequestId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ResponseAddress")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("SentTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("SourceAddress")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("SequenceNumber");
|
||||||
|
|
||||||
|
b.HasIndex("EnqueueTime");
|
||||||
|
|
||||||
|
b.HasIndex("ExpirationTime");
|
||||||
|
|
||||||
|
b.HasIndex("OutboxId", "SequenceNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("OutboxMessage");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("OutboxId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Delivered")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("LastSequenceNumber")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<Guid>("LockId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<byte[]>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("bytea");
|
||||||
|
|
||||||
|
b.HasKey("OutboxId");
|
||||||
|
|
||||||
|
b.HasIndex("Created");
|
||||||
|
|
||||||
|
b.ToTable("OutboxState");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("LargePictureName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("LifeCycleStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MediumPictureName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalPictureName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("SmallPictureName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("PlayLists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("PlayListId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("TrackId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PlayListId");
|
||||||
|
|
||||||
|
b.HasIndex("TrackId");
|
||||||
|
|
||||||
|
b.ToTable("PlayListHasTrack");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AudioFolderName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("AudioTranscodeProcessingStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("LargePictureName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastRetryAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("LifeCycleStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MediumPictureName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalAudioName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalPictureName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("PicturesProcessingStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RetryCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("SmallPictureName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Tracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Bucket")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Upload");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.HasMaxLength(48)
|
||||||
|
.HasColumnType("character varying(48)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(48)
|
||||||
|
.HasColumnType("character varying(48)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(48)
|
||||||
|
.HasColumnType("character varying(48)");
|
||||||
|
|
||||||
|
b.Property<string>("SecondName")
|
||||||
|
.HasMaxLength(48)
|
||||||
|
.HasColumnType("character varying(48)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("TrackId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TrackId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("UserHasTrack");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OutboxId");
|
||||||
|
|
||||||
|
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("InboxMessageId", "InboxConsumerId")
|
||||||
|
.HasPrincipalKey("MessageId", "ConsumerId");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Musify.Domain.Entities.User", "User")
|
||||||
|
.WithMany("PlayLists")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Musify.Domain.Entities.PlayList", "PlayList")
|
||||||
|
.WithMany("PlayListTracks")
|
||||||
|
.HasForeignKey("PlayListId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Musify.Domain.Entities.Track", "Track")
|
||||||
|
.WithMany("PlayListTracks")
|
||||||
|
.HasForeignKey("TrackId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("PlayList");
|
||||||
|
|
||||||
|
b.Navigation("Track");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Musify.Domain.Entities.Track", "Track")
|
||||||
|
.WithMany("UserTracks")
|
||||||
|
.HasForeignKey("TrackId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Musify.Domain.Entities.User", "User")
|
||||||
|
.WithMany("UserTracks")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Track");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayListTracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayListTracks");
|
||||||
|
|
||||||
|
b.Navigation("UserTracks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PlayLists");
|
||||||
|
|
||||||
|
b.Navigation("UserTracks");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLifeCycleStatus : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "LifeCycleStatus",
|
||||||
|
table: "Tracks",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "LifeCycleStatus",
|
||||||
|
table: "PlayLists",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LifeCycleStatus",
|
||||||
|
table: "Tracks");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LifeCycleStatus",
|
||||||
|
table: "PlayLists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -209,6 +209,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("LifeCycleStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("MediumPictureName")
|
b.Property<string>("MediumPictureName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
@@ -297,6 +300,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<DateTime?>("LastRetryAt")
|
b.Property<DateTime?>("LastRetryAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("LifeCycleStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("MediumPictureName")
|
b.Property<string>("MediumPictureName")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|||||||
+32
-3
@@ -97,8 +97,7 @@ Archivos clave:
|
|||||||
- Se añadieron overloads de rutas para construir keys con `userId`.
|
- Se añadieron overloads de rutas para construir keys con `userId`.
|
||||||
- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`.
|
- Los handlers de request presigned URL ahora generan keys usando el prefijo con `userId`.
|
||||||
- Los eventos de workflows (sourceKey) se publican con la key prefijada.
|
- Los eventos de workflows (sourceKey) se publican con la key prefijada.
|
||||||
- El borrado de playlist borra la key original prefijada.
|
- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete.
|
||||||
- El borrado de track (`DeleteTrackActivity`) borra originales usando el `userId` presente en `DeleteTrackArguments`.
|
|
||||||
|
|
||||||
Archivos:
|
Archivos:
|
||||||
- `Application/Configuration/PlayListConfiguration.cs`
|
- `Application/Configuration/PlayListConfiguration.cs`
|
||||||
@@ -109,7 +108,7 @@ Archivos:
|
|||||||
- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs`
|
- `Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs`
|
||||||
- `Application/Tracks/Handler/CreateTrackCommandHandler.cs`
|
- `Application/Tracks/Handler/CreateTrackCommandHandler.cs`
|
||||||
- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs`
|
- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs`
|
||||||
- `Infrastructure/MassTransit/Activities/Tracks/DeleteTrackActivity.cs`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -163,3 +162,33 @@ Archivo:
|
|||||||
## 10) Estado final
|
## 10) Estado final
|
||||||
|
|
||||||
- `dotnet build MusifyBackend.slnx -c Release` ✅
|
- `dotnet build MusifyBackend.slnx -c Release` ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11) Deletes unificados con Workflows (RoutingSlip)
|
||||||
|
|
||||||
|
**Objetivo**: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes.
|
||||||
|
|
||||||
|
### Cambios
|
||||||
|
- Se añadió `LifeCycleStatus` (`Active`/`Removing`) a `Track` y `PlayList` y se generó migración.
|
||||||
|
- Track delete:
|
||||||
|
- Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB.
|
||||||
|
- PlayList delete:
|
||||||
|
- Se añadió `DeletePlayListEvent` + consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB.
|
||||||
|
- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado.
|
||||||
|
- Se eliminaron `RemoveFileEvent`/`RemoveFileConsumer` por quedar sin usos.
|
||||||
|
|
||||||
|
Archivos clave:
|
||||||
|
- `Domain/Entities/LifeCycleStatus.cs`
|
||||||
|
- `Domain/Entities/Track.cs`
|
||||||
|
- `Domain/Entities/PlayList.cs`
|
||||||
|
- `Application/Events/DeletePlayListEvent.cs`
|
||||||
|
- `Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs`
|
||||||
|
- `Application/Tracks/Handler/DeleteTrackCommandHandler.cs`
|
||||||
|
- `Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs`
|
||||||
|
- `Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs`
|
||||||
|
- `Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs`
|
||||||
|
- `Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs`
|
||||||
|
- `Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs`
|
||||||
|
- `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs`
|
||||||
|
- `Infrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs`
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
Actualmente hay dos problemas de diseño en los deletes:
|
Actualmente hay dos problemas de diseño en los deletes:
|
||||||
|
|
||||||
**PlayLists:** el borrado dispara múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego elimina la entidad en DB. Sin workflow, sin trazabilidad.
|
**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad.
|
||||||
|
|
||||||
**Tracks:** ya existe un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hace **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales.
|
**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales.
|
||||||
|
|
||||||
La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes.
|
La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes.
|
||||||
|
|
||||||
@@ -42,30 +42,54 @@ No objetivos (por ahora):
|
|||||||
|
|
||||||
## Diseño propuesto
|
## Diseño propuesto
|
||||||
|
|
||||||
|
## Implementación final (nombres reales)
|
||||||
|
|
||||||
|
Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug):
|
||||||
|
|
||||||
|
### Colas (consumers)
|
||||||
|
- `delete-track` (`DeleteTrackConsumer`)
|
||||||
|
- `delete-playList` (`DeletePlayListConsumer`)
|
||||||
|
|
||||||
|
### Execute endpoints (activities)
|
||||||
|
MassTransit crea las colas de execute como `<endpointName>_execute`.
|
||||||
|
|
||||||
|
- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute`
|
||||||
|
- `mark-track-as-removing` → cola `mark-track-as-removing_execute`
|
||||||
|
- `delete-track-from-db` → cola `delete-track-from-db_execute`
|
||||||
|
- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute`
|
||||||
|
- `delete-playList-from-db` → cola `delete-playList-from-db_execute`
|
||||||
|
|
||||||
|
### Estado de borrado (DB)
|
||||||
|
- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`.
|
||||||
|
- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`).
|
||||||
|
|
||||||
|
> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`.
|
||||||
|
|
||||||
### Actividades compartidas / reutilizables
|
### Actividades compartidas / reutilizables
|
||||||
|
|
||||||
Ya existe (y se mantiene):
|
Ya existe (y se mantiene):
|
||||||
- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip.
|
- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip.
|
||||||
|
|
||||||
Se añaden actividades de DB compartibles por aggregate:
|
Se añaden actividades de DB por aggregate:
|
||||||
- `MarkAsRemovingActivity<TArgs>` — cambia estado de la entidad a `Removing` y persiste. (O una por aggregate si la lógica difiere.)
|
- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
|
||||||
- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones).
|
- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones).
|
||||||
|
- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
|
||||||
- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones).
|
- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones).
|
||||||
|
|
||||||
### Estructura del RoutingSlip para Track
|
### Estructura del RoutingSlip para Track
|
||||||
|
|
||||||
```
|
```
|
||||||
DeleteTrackEvent
|
DeleteTrackEvent
|
||||||
└─► DeleteTrackConsumer
|
└─► DeleteTrackConsumer (queue: `delete-track`)
|
||||||
└─► DeleteTrackRoutingSlipBuilder
|
└─► DeleteTrackRoutingSlipBuilder.BuildAsync(...)
|
||||||
├─ MarkTrackAsRemovingActivity (cambia estado → Removing, persiste)
|
├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`)
|
||||||
├─ RemoveFileFromBucketActivity (original picture)
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture
|
||||||
├─ RemoveFileFromBucketActivity (small picture) ← solo si IsPicturesProcessed
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed)
|
||||||
├─ RemoveFileFromBucketActivity (medium picture) ← solo si IsPicturesProcessed
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed)
|
||||||
├─ RemoveFileFromBucketActivity (large picture) ← solo si IsPicturesProcessed
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed)
|
||||||
├─ RemoveFileFromBucketActivity (original audio)
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio
|
||||||
├─ RemoveFileFromBucketActivity (processed audio) ← solo si IsAudioProcessed
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed)
|
||||||
└─ DeleteTrackFromDbActivity (Remove() + SaveChanges)
|
└─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`)
|
||||||
```
|
```
|
||||||
|
|
||||||
> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue).
|
> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue).
|
||||||
@@ -74,14 +98,14 @@ DeleteTrackEvent
|
|||||||
|
|
||||||
```
|
```
|
||||||
DeletePlayListEvent
|
DeletePlayListEvent
|
||||||
└─► DeletePlayListConsumer
|
└─► DeletePlayListConsumer (queue: `delete-playList`)
|
||||||
└─► DeletePlayListRoutingSlipBuilder
|
└─► DeletePlayListRoutingSlipBuilder.BuildAsync(...)
|
||||||
├─ MarkPlayListAsRemovingActivity (cambia estado → Removing, persiste)
|
├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`)
|
||||||
├─ RemoveFileFromBucketActivity (original — uploads/{userId}/...) ← solo si no es preset
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset
|
||||||
├─ RemoveFileFromBucketActivity (small) ← solo si no es preset
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset
|
||||||
├─ RemoveFileFromBucketActivity (medium) ← solo si no es preset
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset
|
||||||
├─ RemoveFileFromBucketActivity (large) ← solo si no es preset
|
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset
|
||||||
└─ DeletePlayListFromDbActivity (Remove() + SaveChanges, cascade elimina PlayListHasTrack)
|
└─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handler de comando (ambos aggregates)
|
### Handler de comando (ambos aggregates)
|
||||||
@@ -93,14 +117,20 @@ database.PlayLists.Remove(playlist);
|
|||||||
await database.SaveChangesAsync();
|
await database.SaveChangesAsync();
|
||||||
|
|
||||||
// DESPUÉS
|
// DESPUÉS
|
||||||
|
playlist.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId));
|
await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId));
|
||||||
// Nada más. El workflow se encarga de todo.
|
await database.SaveChangesAsync();
|
||||||
|
// El workflow se encarga de borrar ficheros y eliminar en DB.
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// DeleteTrackCommandHandler — ANTES
|
// DeleteTrackCommandHandler — ANTES
|
||||||
// (ya publicaba DeleteTrackEvent, se mantiene igual)
|
// (ya publicaba DeleteTrackEvent, se mantiene igual)
|
||||||
|
|
||||||
|
// DESPUÉS
|
||||||
|
track.LifeCycleStatus = LifeCycleStatus.Removing;
|
||||||
await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId));
|
await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId));
|
||||||
|
await database.SaveChangesAsync();
|
||||||
|
|
||||||
// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica.
|
// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica.
|
||||||
```
|
```
|
||||||
@@ -120,7 +150,7 @@ await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId
|
|||||||
## Idempotencia / reintentos
|
## Idempotencia / reintentos
|
||||||
|
|
||||||
- RoutingSlip + reintentos de endpoint ya configurados en MassTransit.
|
- RoutingSlip + reintentos de endpoint ya configurados en MassTransit.
|
||||||
- Operaciones S3: idempotentes por diseño. Si el key no existe → log `Information` y `Completed()` (no throw).
|
- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw).
|
||||||
- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora).
|
- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora).
|
||||||
- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip.
|
- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user