diff --git a/LANCommander.Server.Services/ArchiveService.cs b/LANCommander.Server.Services/ArchiveService.cs index 6fa32c84..34d3aeb3 100644 --- a/LANCommander.Server.Services/ArchiveService.cs +++ b/LANCommander.Server.Services/ArchiveService.cs @@ -92,28 +92,6 @@ namespace LANCommander.Server.Services await context.UpdateRelationshipAsync(a => a.StorageLocation); }); } - - public override async Task DeleteAsync(Archive archive) - { - FileHelpers.DeleteIfExists(await GetArchiveFileLocationAsync(archive)); - - await cache.ExpireGameCacheAsync(archive.GameId); - await cache.ExpireArchiveCacheAsync(archive.Id); - - await base.DeleteAsync(archive); - } - - public async Task DeleteAsync(Archive archive, StorageLocation storageLocation = null) - { - if (storageLocation == null) - FileHelpers.DeleteIfExists(await GetArchiveFileLocationAsync(archive)); - else - FileHelpers.DeleteIfExists(GetArchiveFileLocation(archive, storageLocation)); - - await cache.ExpireGameCacheAsync(archive.GameId); - - await base.DeleteAsync(archive); - } public async Task ReadManifestAsync(string objectKey) { diff --git a/LANCommander.Server.Services/Interceptors/DeleteArchiveInterceptor.cs b/LANCommander.Server.Services/Interceptors/DeleteArchiveInterceptor.cs new file mode 100644 index 00000000..81e2b0bd --- /dev/null +++ b/LANCommander.Server.Services/Interceptors/DeleteArchiveInterceptor.cs @@ -0,0 +1,104 @@ +using LANCommander.Helpers; +using LANCommander.Server.Data.Models; +using LANCommander.Server.Services.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using ZiggyCreatures.Caching.Fusion; + +namespace LANCommander.Server.Services.Interceptors +{ + /// + /// Ensures the file for an archive is deleted whenever dropped from the database + /// Also ensures that when games/redistributables are deleted, archives are deleted as well + /// + public class DeleteArchiveInterceptor( + ArchiveService archiveService, + IFusionCache cache) : SaveChangesInterceptor + { + private readonly List _pendingArchives = new(); + + public override async ValueTask SavedChangesAsync(SaveChangesCompletedEventData eventData, int result, + CancellationToken cancellationToken = new()) + { + foreach (var archive in _pendingArchives) + { + FileHelpers.DeleteIfExists(await archiveService.GetArchiveFileLocationAsync(archive)); + + await cache.ExpireGameCacheAsync(archive.GameId); + await cache.ExpireArchiveCacheAsync(archive.Id); + } + + return await base.SavedChangesAsync(eventData, result, cancellationToken); + } + + public override async ValueTask> SavingChangesAsync( + DbContextEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + var context = eventData.Context; + + if (context is null) + return await base.SavingChangesAsync(eventData, result, cancellationToken); + + var deletedGames = context.ChangeTracker + .Entries() + .Where(e => e.State == EntityState.Deleted) + .Select(e => e.Entity) + .ToList(); + + foreach (var game in deletedGames) + { + var archives = context.Entry(game).Collection(g => g.Archives!); + + if (!archives.IsLoaded) + await archives.LoadAsync(cancellationToken); + + if (game.Archives != null) + foreach (var archive in game.Archives) + { + var entry = context.Entry(archive); + + if (entry.State == EntityState.Deleted || entry.State == EntityState.Unchanged) + entry.State = EntityState.Deleted; + } + } + + var deletedRedistributables = context.ChangeTracker + .Entries() + .Where(e => e.State == EntityState.Deleted) + .Select(e => e.Entity) + .ToList(); + + foreach (var redistributable in deletedRedistributables) + { + var archives = context.Entry(redistributable).Collection(r => r.Archives!); + + if (!archives.IsLoaded) + await archives.LoadAsync(cancellationToken); + + if (redistributable.Archives != null) + foreach (var archive in redistributable.Archives) + { + var entry = context.Entry(archive); + + if (entry.State == EntityState.Deleted || entry.State == EntityState.Unchanged) + entry.State = EntityState.Deleted; + } + } + + foreach (var entry in context.ChangeTracker.Entries()) + { + var storageLocation = entry.Reference(a => a.StorageLocation); + + if (!storageLocation.IsLoaded) + await storageLocation.LoadAsync(cancellationToken); + + if (entry.State == EntityState.Deleted) + _pendingArchives.Add(entry.Entity); + } + + return await base.SavingChangesAsync(eventData, result, cancellationToken); + } + } +} diff --git a/LANCommander.Server/Startup/Database.cs b/LANCommander.Server/Startup/Database.cs index 1863ed8c..14381998 100644 --- a/LANCommander.Server/Startup/Database.cs +++ b/LANCommander.Server/Startup/Database.cs @@ -3,6 +3,7 @@ using LANCommander.Server.Data; using LANCommander.Server.Data.Enums; using LANCommander.Server.Data.Models; using LANCommander.Server.Services; +using LANCommander.Server.Services.Interceptors; using LANCommander.Server.Services.Models; using LANCommander.Server.Settings.Enums; using LANCommander.Server.Settings.Models; @@ -18,7 +19,11 @@ public static class Database { public static WebApplicationBuilder AddDatabase(this WebApplicationBuilder builder, string[] args) { - builder.Services.AddDbContextFactory(); + builder.Services.AddScoped(); + builder.Services.AddDbContextFactory((sp, options) => + { + options.AddInterceptors(sp.GetRequiredService()); + }); builder.Services.AddDbContext(); builder.Services.AddDatabaseDeveloperPageExceptionFilter();