// 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.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Overlays.Notifications; using osu.Game.Utils; using Realms; namespace osu.Game.Database { /// /// A class which handles exporting legacy user data of a single type from osu-stable. /// public abstract class LegacyModelExporter where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey { /// /// The file extension for exports (including the leading '.'). /// protected abstract string FileExtension { get; } protected Storage UserFileStorage; private readonly Storage exportStorage; protected virtual string GetFilename(TModel item) => item.GetDisplayString(); private readonly RealmAccess realmAccess; public Action? PostNotification { get; set; } // Store the model being exported. private readonly List exportingModels = new List(); /// /// Construct exporter. /// Create a new exporter for each export, otherwise it will cause confusing notifications. /// /// Storage for storing exported files. Basically it is used to provide export stream /// The RealmAccess used to provide the exported file. protected LegacyModelExporter(Storage storage, RealmAccess realm) { exportStorage = storage.GetStorageForDirectory(@"exports"); UserFileStorage = storage.GetStorageForDirectory(@"files"); realmAccess = realm; } /// /// Export the model to default folder. /// /// The model should export. /// /// The Cancellation token that can cancel the exporting. /// If specified CancellationToken, then use it. Otherwise use PostNotification's CancellationToken. /// /// public async Task ExportAsync(TModel model, CancellationToken? cancellationToken = null) { if (!exportingModels.Contains(model)) { exportingModels.Add(model); } else { PostNotification?.Invoke(new SimpleErrorNotification() { Text = "File is being exported" }); return; } string itemFilename = GetFilename(model).GetValidFilename(); IEnumerable existingExports = exportStorage .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}") .Concat(exportStorage.GetDirectories(string.Empty)); string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); bool success = false; ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, Text = "Exporting...", CompletionText = "Export completed" }; notification.CompletionClickAction += () => exportStorage.PresentFileExternally(filename); PostNotification?.Invoke(notification); try { using (var stream = exportStorage.CreateFileSafely(filename)) { success = await ExportToStreamAsync(model, stream, notification, cancellationToken ?? notification.CancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) { success = false; } finally { if (!success) { exportStorage.Delete(filename); } exportingModels.Remove(model); } } /// /// Export model to stream. /// /// The medel which have . /// The stream to export. /// The notification will displayed to the user /// The Cancellation token that can cancel the exporting. /// Whether the export was successful public async Task ExportToStreamAsync(TModel model, Stream stream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) { ProgressNotification notify = notification ?? new ProgressNotification(); Guid id = model.ID; return await Task.Run(() => { realmAccess.Run(r => { TModel refetchModel = r.Find(id); ExportToStream(refetchModel, stream, notify, cancellationToken); }); }, cancellationToken).ContinueWith(t => { if (t.IsFaulted) { notify.State = ProgressNotificationState.Cancelled; Logger.Error(t.Exception, "An error occurred while exporting"); return false; } notify.CompletionText = "Export Complete, Click to open the folder"; notify.State = ProgressNotificationState.Completed; return true; }, cancellationToken).ConfigureAwait(false); } /// /// Exports an item to Stream. /// Override if custom export method is required. /// /// The item to export. /// The output stream to export to. /// The notification will displayed to the user /// The Cancellation token that can cancel the exporting. protected abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification notification, CancellationToken cancellationToken = default); } }