mirror of
https://github.com/ppy/osu
synced 2024-12-25 08:12:41 +00:00
531794b26b
Falls into the age-old trap of attempting to retrieve an item from realm without first ensuring that realm is in an up-to-date state. Consider this scenario: - Editor is entered from main menu, causing it to create a new beatmap from its async `load()` method. - Editor opens correctly, then main thread performs a file operations on the same beatmap. - Main thread is potentially not refreshed yet, and will result in `null` instance when performing the re-fetch in `performFileOperation`. I've fixed this by using the safe implementation inside `RealmLive<T>`. Feels like we want this is one place which is always used as the correct method. On a quick search, there are 10-20 other usages of `Realm.Find<T>` which could also have similar issues, but it'll be a bit of a pain to go through and fix each of these. In 99.9% of cases, the accesses are on instances which couldn't have just been created (or the usage of recently-imported/created is blocked by realm subscription flows, ie. baetmap import) so I'm not touching them for now. Something to keep in mind when working with realm going forward though.
214 lines
7.3 KiB
C#
214 lines
7.3 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using osu.Framework.Platform;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Extensions;
|
|
using osu.Game.Models;
|
|
using osu.Game.Overlays.Notifications;
|
|
using Realms;
|
|
|
|
namespace osu.Game.Database
|
|
{
|
|
public class ModelManager<TModel> : IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
|
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
|
{
|
|
/// <summary>
|
|
/// Temporarily pause imports to avoid performance overheads affecting gameplay scenarios.
|
|
/// </summary>
|
|
public virtual bool PauseImports { get; set; }
|
|
|
|
protected RealmAccess Realm { get; }
|
|
|
|
private readonly RealmFileStore realmFileStore;
|
|
|
|
public ModelManager(Storage storage, RealmAccess realm)
|
|
{
|
|
realmFileStore = new RealmFileStore(realm, storage);
|
|
Realm = realm;
|
|
}
|
|
|
|
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
|
|
performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!));
|
|
|
|
public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) =>
|
|
performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!));
|
|
|
|
public void AddFile(TModel item, Stream contents, string filename) =>
|
|
performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!));
|
|
|
|
private void performFileOperation(TModel item, Action<TModel> operation)
|
|
{
|
|
// While we are detaching so often, this seems like the easiest way to keep things in sync.
|
|
// This method should be removed as soon as all the surrounding pieces support non-detached operations.
|
|
if (!item.IsManaged)
|
|
{
|
|
// We use RealmLive here as it handled re-retrieval and refreshing of realm if required.
|
|
new RealmLive<TModel>(item.ID, Realm).PerformWrite(i =>
|
|
{
|
|
operation(i);
|
|
|
|
item.Files.Clear();
|
|
item.Files.AddRange(i.Files.Detach());
|
|
});
|
|
}
|
|
else
|
|
operation(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a file from within an ongoing realm transaction.
|
|
/// </summary>
|
|
public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm)
|
|
{
|
|
item.Files.Remove(file);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replace a file from within an ongoing realm transaction.
|
|
/// </summary>
|
|
public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm)
|
|
{
|
|
file.File = realmFileStore.Add(contents, realm);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten.
|
|
/// </summary>
|
|
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
|
|
{
|
|
var existing = item.GetFile(filename);
|
|
|
|
if (existing != null)
|
|
{
|
|
ReplaceFile(existing, contents, realm);
|
|
return;
|
|
}
|
|
|
|
var file = realmFileStore.Add(contents, realm);
|
|
var namedUsage = new RealmNamedFileUsage(file, filename);
|
|
|
|
item.Files.Add(namedUsage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete multiple items.
|
|
/// This will post notifications tracking progress.
|
|
/// </summary>
|
|
public void Delete(List<TModel> items, bool silent = false)
|
|
{
|
|
if (items.Count == 0) return;
|
|
|
|
var notification = new ProgressNotification
|
|
{
|
|
Progress = 0,
|
|
Text = $"Preparing to delete all {HumanisedModelName}s...",
|
|
CompletionText = $"Deleted all {HumanisedModelName}s!",
|
|
State = ProgressNotificationState.Active,
|
|
};
|
|
|
|
if (!silent)
|
|
PostNotification?.Invoke(notification);
|
|
|
|
int i = 0;
|
|
|
|
foreach (var b in items)
|
|
{
|
|
if (notification.State == ProgressNotificationState.Cancelled)
|
|
// user requested abort
|
|
return;
|
|
|
|
notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
|
|
|
|
Delete(b);
|
|
|
|
notification.Progress = (float)i / items.Count;
|
|
}
|
|
|
|
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, bool silent = false)
|
|
{
|
|
if (!items.Any()) return;
|
|
|
|
var notification = new ProgressNotification
|
|
{
|
|
CompletionText = "Restored all deleted items!",
|
|
Progress = 0,
|
|
State = ProgressNotificationState.Active,
|
|
};
|
|
|
|
if (!silent)
|
|
PostNotification?.Invoke(notification);
|
|
|
|
int i = 0;
|
|
|
|
foreach (var item in items)
|
|
{
|
|
if (notification.State == ProgressNotificationState.Cancelled)
|
|
// user requested abort
|
|
return;
|
|
|
|
notification.Text = $"Restoring ({++i} of {items.Count})";
|
|
|
|
Undelete(item);
|
|
|
|
notification.Progress = (float)i / items.Count;
|
|
}
|
|
|
|
notification.State = ProgressNotificationState.Completed;
|
|
}
|
|
|
|
public bool Delete(TModel item)
|
|
{
|
|
// Importantly, begin the realm write *before* re-fetching, else the update realm may not be in a consistent state
|
|
// (ie. if an async import finished very recently).
|
|
return Realm.Write(realm =>
|
|
{
|
|
TModel? processableItem = item;
|
|
if (!processableItem.IsManaged)
|
|
processableItem = realm.Find<TModel>(item.ID);
|
|
|
|
if (processableItem?.DeletePending != false)
|
|
return false;
|
|
|
|
processableItem.DeletePending = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public void Undelete(TModel item)
|
|
{
|
|
// Importantly, begin the realm write *before* re-fetching, else the update realm may not be in a consistent state
|
|
// (ie. if an async import finished very recently).
|
|
Realm.Write(realm =>
|
|
{
|
|
TModel? processableItem = item;
|
|
if (!processableItem.IsManaged)
|
|
processableItem = realm.Find<TModel>(item.ID);
|
|
|
|
if (processableItem?.DeletePending != true)
|
|
return;
|
|
|
|
processableItem.DeletePending = false;
|
|
});
|
|
}
|
|
|
|
public virtual bool IsAvailableLocally(TModel model) => true;
|
|
|
|
public Action<Notification>? PostNotification { get; set; }
|
|
|
|
public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLowerInvariant()}";
|
|
}
|
|
}
|