From d3dd31dadb0d8312475bc2fc82c4595340217582 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 13:30:17 +0900 Subject: [PATCH] Make deletion and purging logic even more global --- .../Visual/TestCasePlaySongSelect.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 52 +------------- osu.Game/Beatmaps/BeatmapStore.cs | 38 ++++------- osu.Game/Database/ArchiveModelManager.cs | 68 ++++++++++++++++--- osu.Game/Database/DatabaseBackedStore.cs | 2 +- .../Database/MutableDatabaseBackedStore.cs | 35 ++++++++++ osu.Game/IO/FileStore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Sections/Maintenance/GeneralSettings.cs | 4 +- 9 files changed, 114 insertions(+), 91 deletions(-) diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 8bb0d152f6..13b2be9fdb 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual { if (deleteMaps) { - manager.DeleteAll(); + manager.Delete(manager.GetAllUsableBeatmapSets()); game.Beatmap.SetDefault(); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8bc1f72c1f..4a6b6909b9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -71,8 +71,6 @@ namespace osu.Game.Beatmaps this.rulesets = rulesets; this.api = api; - - beatmaps.Cleanup(); } protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) @@ -102,7 +100,7 @@ namespace osu.Game.Beatmaps if (existingOnlineId != null) { Delete(existingOnlineId); - beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); + beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); } } @@ -193,12 +191,6 @@ namespace osu.Game.Beatmaps /// The object if it exists, or null. public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); - /// - /// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently. - /// - /// The beatmap set to update. - public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap); - /// /// Delete a beatmap difficulty. /// @@ -239,13 +231,6 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); - /// - /// Refresh an existing instance of a from the store. - /// - /// A stale instance. - /// A fresh instance. - public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); - /// /// Returns a list of all usable s. /// @@ -294,41 +279,6 @@ namespace osu.Game.Beatmaps await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); } - /// - /// Delete all beatmaps. - /// This will post notifications tracking progress. - /// - public void DeleteAll() - { - var maps = GetAllUsableBeatmapSets(); - - if (maps.Count == 0) return; - - var notification = new ProgressNotification - { - Progress = 0, - CompletionText = "Deleted all beatmaps!", - State = ProgressNotificationState.Active, - }; - - PostNotification?.Invoke(notification); - - int i = 0; - - foreach (var b in maps) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - notification.Text = $"Deleting ({i} of {maps.Count})"; - notification.Progress = (float)++i / maps.Count; - Delete(b); - } - - notification.State = ProgressNotificationState.Completed; - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 3e4840f4e1..e695c3bf28 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -2,8 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; @@ -63,32 +63,24 @@ namespace osu.Game.Beatmaps return true; } - public override void Cleanup() => Cleanup(_ => true); - - public void Cleanup(Expression> query) + protected override IQueryable AddIncludesForDeletion(IQueryable query) { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; + return base.AddIncludesForDeletion(query) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); + } - var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) - .Where(query) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Metadata).ToList(); + protected override void Purge(List items, OsuDbContext context) + { + // metadata is M-N so we can't rely on cascades + context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata)); + context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); - if (!purgeable.Any()) return; + // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. + context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - // metadata is M-N so we can't rely on cascades - context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); - context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); - - // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. - context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - - // cascades down to beatmaps. - context.BeatmapSetInfo.RemoveRange(purgeable); - } + base.Purge(items, context); } public IQueryable BeatmapSets => ContextFactory.Get().BeatmapSetInfo diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9c558a6c12..31eab79127 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -62,6 +62,8 @@ namespace osu.Game.Database if (importHost != null) ipc = new ArchiveImportIPCChannel(importHost, this); + + ModelStore.PurgeDeletable(); } /// @@ -154,6 +156,13 @@ namespace osu.Game.Database /// The model to be imported. public void Import(TModel item) => ModelStore.Add(item); + /// + /// Perform an update of the specified item. + /// TODO: Support file changes. + /// + /// The item to update. + public void Update(TModel item) => ModelStore.Update(item); + /// /// Delete an item from the manager. /// Is a no-op for already deleted items. @@ -180,14 +189,48 @@ namespace osu.Game.Database } /// - /// Restore all items that were previously deleted. + /// Delete multiple items. /// This will post notifications tracking progress. /// - public void UndeleteAll() + public void Delete(List items) { - var deletedItems = queryModel().Where(m => m.DeletePending).ToList(); + if (items.Count == 0) return; - if (!deletedItems.Any()) return; + var notification = new ProgressNotification + { + Progress = 0, + CompletionText = "Deleted all beatmaps!", + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + using (ContextFactory.GetForWrite()) + { + foreach (var b in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Deleting ({i} of {items.Count})"; + notification.Progress = (float)++i / items.Count; + Delete(b); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void Undelete(List items) + { + if (!items.Any()) return; var notification = new ProgressNotification { @@ -200,15 +243,18 @@ namespace osu.Game.Database int i = 0; - foreach (var item in deletedItems) + using (ContextFactory.GetForWrite()) { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; + foreach (var item in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; - notification.Text = $"Restoring ({i} of {deletedItems.Count})"; - notification.Progress = (float)++i / deletedItems.Count; - Undelete(item); + notification.Text = $"Restoring ({i} of {items.Count})"; + notification.Progress = (float)++i / items.Count; + Undelete(item); + } } notification.State = ProgressNotificationState.Completed; diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index cf46b66422..a1ed992f03 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -49,7 +49,7 @@ namespace osu.Game.Database /// /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. /// - public virtual void Cleanup() + public virtual void PurgeDeletable() { } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index c6af1aa475..9de6068d10 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using osu.Framework.Platform; namespace osu.Game.Database @@ -72,5 +75,37 @@ namespace osu.Game.Database ItemAdded?.Invoke(item); return true; } + + protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; + + protected virtual void Purge(List items, OsuDbContext context) + { + // cascades down to beatmaps. + context.RemoveRange(items); + } + + /// + /// Purge items in a pending delete state. + /// + /// An optional query limiting the scope of the purge. + public void PurgeDeletable(Expression> query = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + var lookup = context.Set().Where(s => s.DeletePending); + + if (query != null) lookup = lookup.Where(query); + + AddIncludesForDeletion(lookup); + + var purgeable = lookup.ToList(); + + if (!purgeable.Any()) return; + + Purge(purgeable, context); + } + } } } diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index ab81ba4851..6f262fd8fa 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -90,7 +90,7 @@ namespace osu.Game.IO } } - public override void Cleanup() + public override void PurgeDeletable() { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 505577416d..ce50f160f7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -172,7 +172,7 @@ namespace osu.Game API.Register(this); - FileStore.Cleanup(); + FileStore.PurgeDeletable(); } private void runMigrations() diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1223310c74..eec99dc886 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => { deleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); })); } }, @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.DeletePending))).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, };