Make deletion and purging logic even more global

This commit is contained in:
Dean Herbert
2018-02-15 13:30:17 +09:00
parent d340509b1d
commit d3dd31dadb
9 changed files with 114 additions and 91 deletions

View File

@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
{ {
if (deleteMaps) if (deleteMaps)
{ {
manager.DeleteAll(); manager.Delete(manager.GetAllUsableBeatmapSets());
game.Beatmap.SetDefault(); game.Beatmap.SetDefault();
} }

View File

@ -71,8 +71,6 @@ namespace osu.Game.Beatmaps
this.rulesets = rulesets; this.rulesets = rulesets;
this.api = api; this.api = api;
beatmaps.Cleanup();
} }
protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) protected override void Populate(BeatmapSetInfo model, ArchiveReader archive)
@ -102,7 +100,7 @@ namespace osu.Game.Beatmaps
if (existingOnlineId != null) if (existingOnlineId != null)
{ {
Delete(existingOnlineId); Delete(existingOnlineId);
beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
} }
} }
@ -193,12 +191,6 @@ namespace osu.Game.Beatmaps
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns> /// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID);
/// <summary>
/// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently.
/// </summary>
/// <param name="beatmapSet">The beatmap set to update.</param>
public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap);
/// <summary> /// <summary>
/// Delete a beatmap difficulty. /// Delete a beatmap difficulty.
/// </summary> /// </summary>
@ -239,13 +231,6 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns> /// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query);
/// <summary>
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
/// </summary>
/// <param name="beatmapSet">A stale instance.</param>
/// <returns>A fresh instance.</returns>
public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID);
/// <summary> /// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. /// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary> /// </summary>
@ -294,41 +279,6 @@ namespace osu.Game.Beatmaps
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning);
} }
/// <summary>
/// Delete all beatmaps.
/// This will post notifications tracking progress.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
/// </summary> /// </summary>

View File

@ -2,8 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Game.Database; using osu.Game.Database;
@ -63,32 +63,24 @@ namespace osu.Game.Beatmaps
return true; return true;
} }
public override void Cleanup() => Cleanup(_ => true); protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query)
public void Cleanup(Expression<Func<BeatmapSetInfo, bool>> query)
{ {
using (var usage = ContextFactory.GetForWrite()) return base.AddIncludesForDeletion(query)
{
var context = usage.Context;
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.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Metadata).ToList(); .Include(s => s.Metadata);
}
if (!purgeable.Any()) return;
protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
{
// metadata is M-N so we can't rely on cascades // metadata is M-N so we can't rely on cascades
context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata));
context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); context.BeatmapMetadata.RemoveRange(items.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. // 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))); context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
// cascades down to beatmaps. base.Purge(items, context);
context.BeatmapSetInfo.RemoveRange(purgeable);
}
} }
public IQueryable<BeatmapSetInfo> BeatmapSets => ContextFactory.Get().BeatmapSetInfo public IQueryable<BeatmapSetInfo> BeatmapSets => ContextFactory.Get().BeatmapSetInfo

View File

@ -62,6 +62,8 @@ namespace osu.Game.Database
if (importHost != null) if (importHost != null)
ipc = new ArchiveImportIPCChannel(importHost, this); ipc = new ArchiveImportIPCChannel(importHost, this);
ModelStore.PurgeDeletable();
} }
/// <summary> /// <summary>
@ -154,6 +156,13 @@ namespace osu.Game.Database
/// <param name="item">The model to be imported.</param> /// <param name="item">The model to be imported.</param>
public void Import(TModel item) => ModelStore.Add(item); public void Import(TModel item) => ModelStore.Add(item);
/// <summary>
/// Perform an update of the specified item.
/// TODO: Support file changes.
/// </summary>
/// <param name="item">The item to update.</param>
public void Update(TModel item) => ModelStore.Update(item);
/// <summary> /// <summary>
/// Delete an item from the manager. /// Delete an item from the manager.
/// Is a no-op for already deleted items. /// Is a no-op for already deleted items.
@ -180,14 +189,48 @@ namespace osu.Game.Database
} }
/// <summary> /// <summary>
/// Restore all items that were previously deleted. /// Delete multiple items.
/// This will post notifications tracking progress. /// This will post notifications tracking progress.
/// </summary> /// </summary>
public void UndeleteAll() public void Delete(List<TModel> 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;
}
/// <summary>
/// Restore multiple items that were previously deleted.
/// This will post notifications tracking progress.
/// </summary>
public void Undelete(List<TModel> items)
{
if (!items.Any()) return;
var notification = new ProgressNotification var notification = new ProgressNotification
{ {
@ -200,16 +243,19 @@ namespace osu.Game.Database
int i = 0; int i = 0;
foreach (var item in deletedItems) using (ContextFactory.GetForWrite())
{
foreach (var item in items)
{ {
if (notification.State == ProgressNotificationState.Cancelled) if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort // user requested abort
return; return;
notification.Text = $"Restoring ({i} of {deletedItems.Count})"; notification.Text = $"Restoring ({i} of {items.Count})";
notification.Progress = (float)++i / deletedItems.Count; notification.Progress = (float)++i / items.Count;
Undelete(item); Undelete(item);
} }
}
notification.State = ProgressNotificationState.Completed; notification.State = ProgressNotificationState.Completed;
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary.
/// </summary> /// </summary>
public virtual void Cleanup() public virtual void PurgeDeletable()
{ {
} }
} }

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using osu.Framework.Platform; using osu.Framework.Platform;
namespace osu.Game.Database namespace osu.Game.Database
@ -72,5 +75,37 @@ namespace osu.Game.Database
ItemAdded?.Invoke(item); ItemAdded?.Invoke(item);
return true; return true;
} }
protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;
protected virtual void Purge(List<T> items, OsuDbContext context)
{
// cascades down to beatmaps.
context.RemoveRange(items);
}
/// <summary>
/// Purge items in a pending delete state.
/// </summary>
/// <param name="query">An optional query limiting the scope of the purge.</param>
public void PurgeDeletable(Expression<Func<T, bool>> query = null)
{
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
var lookup = context.Set<T>().Where(s => s.DeletePending);
if (query != null) lookup = lookup.Where(query);
AddIncludesForDeletion(lookup);
var purgeable = lookup.ToList();
if (!purgeable.Any()) return;
Purge(purgeable, context);
}
}
} }
} }

View File

@ -90,7 +90,7 @@ namespace osu.Game.IO
} }
} }
public override void Cleanup() public override void PurgeDeletable()
{ {
using (var usage = ContextFactory.GetForWrite()) using (var usage = ContextFactory.GetForWrite())
{ {

View File

@ -172,7 +172,7 @@ namespace osu.Game
API.Register(this); API.Register(this);
FileStore.Cleanup(); FileStore.PurgeDeletable();
} }
private void runMigrations() private void runMigrations()

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{ {
deleteButton.Enabled.Value = false; 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 = () => Action = () =>
{ {
undeleteButton.Enabled.Value = false; 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));
} }
}, },
}; };