Safe code and changes

This commit is contained in:
2026-05-14 01:00:44 +02:00
parent 1ecf4a872e
commit f6ac45cb79
25 changed files with 568 additions and 266 deletions
@@ -34,13 +34,35 @@ namespace Musify.Application.Tracks.Handler
var fullPictureKey = trackConfiguration.Routes.BuildOriginalPicturePath(trackEntity.OriginalPictureName);
var fullAudioKey = trackConfiguration.Routes.BuildOriginalAudioPath(trackEntity.OriginalAudioName);
await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken);
var uploadResult = await UploadFilesAsync(request, fullPictureKey, fullAudioKey, cancellationToken);
if (!uploadResult.IsSuccess)
return uploadResult;
await PublishUpdateEvent(trackEntity, cancellationToken);
await PublishTranscodeEvent(trackEntity, cancellationToken);
try
{
await database.Tracks.AddAsync(trackEntity, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to save track {Title} after uploading files", request.Title);
await RollbackFilesAsync(fullPictureKey, fullAudioKey, cancellationToken);
return Result.Error($"Failed to create track for {request.Title}");
}
await database.Tracks.AddAsync(trackEntity, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
var publishPictureResult = await PublishUpdateEvent(trackEntity, cancellationToken);
if (!publishPictureResult.IsSuccess)
{
await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken);
return Result.Error($"Failed to create track for {request.Title}");
}
var publishTranscodeResult = await PublishTranscodeEvent(trackEntity, cancellationToken);
if (!publishTranscodeResult.IsSuccess)
{
await RollbackTrackAfterPublishFailureAsync(trackEntity, fullPictureKey, fullAudioKey, cancellationToken);
return Result.Error($"Failed to create track for {request.Title}");
}
return Result.Created(TrackResponse.FromEntity(trackEntity));
}
@@ -91,7 +113,7 @@ namespace Musify.Application.Tracks.Handler
logger.LogInformation("Rolled back uploaded files {PictureKey}, {AudioKey}", pictureKey, audioKey);
}
async Task PublishUpdateEvent(Track track, CancellationToken cancellationToken)
async Task<Result> PublishUpdateEvent(Track track, CancellationToken cancellationToken)
{
try
{
@@ -111,14 +133,16 @@ namespace Musify.Application.Tracks.Handler
trackConfiguration.Routes.LargePicturesPath,
trackConfiguration.PicturesSizes.LargePictureWidth,
trackConfiguration.PicturesSizes.LargePictureHeight)), cancellationToken);
return Result.Success();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to publish track picture update event for track {TrackId}", track.Id);
return Result.Error($"Failed to publish track picture update event for track {track.Id}");
}
}
async Task PublishTranscodeEvent(Track track, CancellationToken cancellationToken)
async Task<Result> PublishTranscodeEvent(Track track, CancellationToken cancellationToken)
{
var destinationFolderAudio = trackConfiguration.Routes.BuildProcessedAudioPath(Guid.NewGuid().ToString());
@@ -130,10 +154,38 @@ namespace Musify.Application.Tracks.Handler
trackConfiguration.Routes.BuildOriginalAudioPath(track.OriginalAudioName),
storageConfiguration.Bucket,
destinationFolderAudio), cancellationToken);
return Result.Success();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to publish track audio update event for track {TrackId}", track.Id);
return Result.Error($"Failed to publish track audio update event for track {track.Id}");
}
}
async Task RollbackTrackAfterPublishFailureAsync(
Track track,
string pictureKey,
string audioKey,
CancellationToken cancellationToken)
{
try
{
database.Tracks.Remove(track);
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to rollback track {TrackId} persistence after publish failure", track.Id);
}
try
{
await RollbackFilesAsync(pictureKey, audioKey, cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to rollback uploaded files after publish failure for track {TrackId}", track.Id);
}
}
}
@@ -24,7 +24,10 @@ namespace Musify.Application.Tracks.Handler
return Result.NotFound();
}
if (track.Id != request.UserId)
var isOwner = await database.UserHasTracks
.AsNoTracking()
.AnyAsync(ut => ut.TrackId == request.TrackId && ut.UserId == request.UserId, cancellationToken);
if (!isOwner)
{
logger.LogWarning("User {UserId} unauthorized to delete track {TrackId}", request.UserId, request.TrackId);
return Result.Unauthorized();
@@ -37,13 +40,46 @@ namespace Musify.Application.Tracks.Handler
return Result.Conflict("Track is currently being processed and cannot be deleted");
}
var previousPicturesStatus = track.PicturesProcessingStatus;
var previousAudioStatus = track.AudioTranscodeProcessingStatus;
track.PicturesProcessingStatus = ProcessingStatus.Processing;
track.AudioTranscodeProcessingStatus = ProcessingStatus.Processing;
await database.SaveChangesAsync(cancellationToken);
await eventBus.PublishAsync(
new DeleteTrackEvent(track.Id, request.UserId),
cancellationToken);
try
{
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to mark track {TrackId} as processing before deletion", request.TrackId);
return Result.Error($"Failed to delete track {request.TrackId}");
}
try
{
await eventBus.PublishAsync(
new DeleteTrackEvent(track.Id, request.UserId),
cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to publish delete track event for track {TrackId}", request.TrackId);
track.PicturesProcessingStatus = previousPicturesStatus;
track.AudioTranscodeProcessingStatus = previousAudioStatus;
try
{
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception rollbackException)
{
logger.LogError(rollbackException, "Failed to rollback track {TrackId} processing flags after publish failure", request.TrackId);
}
return Result.Error($"Failed to delete track {request.TrackId}");
}
return Result.NoContent();
}
@@ -16,15 +16,17 @@ namespace Musify.Application.Tracks.Handler
{
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
var normalizedName = request.Name?.Trim().ToUpperInvariant() ?? string.Empty;
var tracksQuery = database.UserHasTracks
.AsNoTracking()
.Include(ut => ut.Track)
.Where(p => p.UserId == request.UserId);
if (!string.IsNullOrWhiteSpace(normalizedName))
if (request.Name is not null)
{
var normalizedName = request.Name.Trim().ToUpperInvariant();
tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName));
}
var totalCount = await tracksQuery.CountAsync(cancellationToken);
@@ -17,15 +17,16 @@ namespace Musify.Application.Tracks.Handler
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
var totalCount = await database.PlayLists.CountAsync(cancellationToken);
var pagedPlayLists = await database.UserHasTracks
var tracksQuery = database.UserHasTracks
.AsNoTracking()
.OrderBy(t => t.Id)
.Include(ut => ut.Track)
.Select(t => TrackResponse.FromEntity(t.Track))
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
.Select(t => TrackResponse.FromEntity(t.Track));
return Result.Success(PaginatedResponse<TrackResponse>.FromPagedList(pagedPlayLists));
var totalCount = await tracksQuery.CountAsync(cancellationToken);
var pagedTracks = await tracksQuery.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
return Result.Success(PaginatedResponse<TrackResponse>.FromPagedList(pagedTracks));
}
}
}
@@ -7,7 +7,7 @@ namespace Musify.Application.Tracks.Queries
{
public record GetTracksByUserIdQuery(
Guid UserId,
string Name = "",
string? Name = null,
int PageNumber = 1,
int PageSize = 10)
: IQuery<Result<PaginatedResponse<TrackResponse> > >;