// Copyright (c) ppy Pty Ltd . 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 : IModelManager, IModelFileManager where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete { /// /// Temporarily pause imports to avoid performance overheads affecting gameplay scenarios. /// 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 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) { // 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.Realm.Write(realm => { var managed = realm.Find(item.ID); Debug.Assert(managed != null); operation(managed); item.Files.Clear(); item.Files.AddRange(managed.Files.Detach()); }); } else operation(item); } /// /// Delete a file from within an ongoing realm transaction. /// public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm) { item.Files.Remove(file); } /// /// Replace a file from within an ongoing realm transaction. /// public void ReplaceFile(RealmNamedFileUsage file, Stream contents, Realm realm) { file.File = realmFileStore.Add(contents, realm); } /// /// Add a file from within an ongoing realm transaction. If the file already exists, it is overwritten. /// 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); } /// /// Delete multiple items. /// This will post notifications tracking progress. /// public void Delete(List 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; } /// /// Restore multiple items that were previously deleted. /// This will post notifications tracking progress. /// public void Undelete(List 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 => { if (!item.IsManaged) item = realm.Find(item.ID); if (item?.DeletePending != false) return false; item.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 => { if (!item.IsManaged) item = realm.Find(item.ID); if (item?.DeletePending != true) return; item.DeletePending = false; }); } public virtual bool IsAvailableLocally(TModel model) => true; public Action? PostNotification { get; set; } public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLowerInvariant()}"; } }