Simplify infrastructure services and move all recurring jobs to Hangfire

This commit is contained in:
2026-09-24 12:18:57 +02:00
parent 0fa033faff
commit 0cba32f776
24 changed files with 361 additions and 413 deletions
@@ -2,11 +2,12 @@ namespace Musify.Application.Contracts;
public interface IStorageService
{
/// <summary>Opens an object for reading, or returns null when it does not exist.</summary>
public Task<Stream?> GetFileAsync(string bucket, string key, CancellationToken cancellationToken);
public Task<ObjectMetaData?> HeadObjectAsync(string bucket, string key, CancellationToken cancellationToken);
public Task<string> GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, CancellationToken cancellationToken, bool preventOverwrite = true);
public Task<string> GetUploadUrlAsync(string bucket, string key, string contentType, TimeSpan expirationTime, bool preventOverwrite = true, CancellationToken cancellationToken = default);
public Task UploadFileAsync(string filePath, string bucket, string key, CancellationToken cancellationToken);
@@ -1,4 +1,3 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -10,17 +9,17 @@ using Musify.Domain.ValueObjects;
namespace Musify.Application.Mixes;
public record GenerateMixesForUserCommand(long UserId)
: ICommand<ErrorOr<Success>>;
: ICommand;
public class GenerateMixesForUserCommandHandler(
IDatabase database,
MixConfiguration mixConfiguration,
ILogger<GenerateMixesForUserCommandHandler> logger)
: ICommandHandler<GenerateMixesForUserCommand, ErrorOr<Success>>
: ICommandHandler<GenerateMixesForUserCommand>
{
private sealed record MixDraft(MixKind Kind, IReadOnlyList<Guid> TrackIds);
public async ValueTask<ErrorOr<Success>> Handle(GenerateMixesForUserCommand request, CancellationToken cancellationToken)
public async ValueTask<Unit> Handle(GenerateMixesForUserCommand request, CancellationToken cancellationToken)
{
var userId = request.UserId;
@@ -33,7 +32,7 @@ public class GenerateMixesForUserCommandHandler(
if (libraryTrackIds.Count == 0)
{
logger.LogInformation("User {UserId} has no tracks yet, skipping mix generation", userId);
return Result.Success;
return Unit.Value;
}
var historyTrackIds = await database.ListeningHistories
@@ -63,7 +62,7 @@ public class GenerateMixesForUserCommandHandler(
if (drafts.Count == 0)
{
logger.LogInformation("No candidate songs found for user {UserId}, skipping mix generation", userId);
return Result.Success;
return Unit.Value;
}
await ReplaceMixesAsync(userId, drafts, cancellationToken);
@@ -71,7 +70,7 @@ public class GenerateMixesForUserCommandHandler(
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Generated {Count} mixes for user {UserId}", drafts.Count, userId);
return Result.Success;
return Unit.Value;
}
private List<Guid> Take(IEnumerable<Guid> trackIds) =>
@@ -69,7 +69,7 @@ public sealed class UploadIntentIssuer(
foreach (var item in planned)
{
var uploadUrl = await storageService.GetUploadUrlAsync(
storageConfiguration.Bucket, item.Key, item.Request.ContentType, expiresIn, cancellationToken);
storageConfiguration.Bucket, item.Key, item.Request.ContentType, expiresIn, cancellationToken: cancellationToken);
var intent = new UploadIntent
{
@@ -0,0 +1,38 @@
using Mediator;
using Microsoft.Extensions.Logging;
using Musify.Application.Configuration;
using Musify.Application.Contracts;
namespace Musify.Application.UploadIntents;
/// <summary>Deletes temporary upload objects that nobody claimed within the retention window.</summary>
public record DeleteStaleTempUploadsCommand : ICommand<int>;
public class DeleteStaleTempUploadsCommandHandler(
IStorageService storageService,
ApplicationStorageConfiguration storageConfiguration,
UploadIntentConfiguration uploadIntentConfiguration,
ILogger<DeleteStaleTempUploadsCommandHandler> logger)
: ICommandHandler<DeleteStaleTempUploadsCommand, int>
{
public async ValueTask<int> Handle(DeleteStaleTempUploadsCommand request, CancellationToken cancellationToken)
{
var cutOff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays);
var deleted = 0;
await foreach (var (key, lastModified) in storageService.ListObjectsAsync(
storageConfiguration.Bucket, uploadIntentConfiguration.TempRootPrefix, cancellationToken))
{
if (lastModified >= cutOff)
continue;
await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken);
deleted++;
}
if (deleted > 0)
logger.LogInformation("Deleted {Deleted} orphaned temp uploads", deleted);
return deleted;
}
}
@@ -0,0 +1,46 @@
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Configuration;
using Musify.Application.Contracts;
using Musify.Domain.ValueObjects;
namespace Musify.Application.UploadIntents;
/// <summary>Marks overdue upload intents as expired and deletes the ones expired for longer than the retention window.</summary>
public record ExpireUploadIntentsCommand : ICommand<int>;
public class ExpireUploadIntentsCommandHandler(
IDatabase database,
UploadIntentConfiguration uploadIntentConfiguration,
ILogger<ExpireUploadIntentsCommandHandler> logger)
: ICommandHandler<ExpireUploadIntentsCommand, int>
{
public async ValueTask<int> Handle(ExpireUploadIntentsCommand request, CancellationToken cancellationToken)
{
var now = DateTime.UtcNow;
var expiredIntents = await database.UploadIntents
.Where(intent => intent.Status == UploadIntentStatus.Issued && intent.ExpiresAt < now)
.ToListAsync(cancellationToken);
foreach (var intent in expiredIntents)
intent.Status = UploadIntentStatus.Expired;
var retentionCutoff = now.AddDays(-uploadIntentConfiguration.ExpiredIntentsRetentionDays);
var oldExpiredIntents = await database.UploadIntents
.Where(intent => intent.Status == UploadIntentStatus.Expired && intent.ExpiresAt < retentionCutoff)
.ToListAsync(cancellationToken);
database.UploadIntents.RemoveRange(oldExpiredIntents);
if (expiredIntents.Count == 0 && oldExpiredIntents.Count == 0)
return 0;
await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Marked {Expired} upload intents as expired and deleted {Deleted} old ones",
expiredIntents.Count, oldExpiredIntents.Count);
return expiredIntents.Count + oldExpiredIntents.Count;
}
}