// 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 exporting. private static readonly List exporting_models = 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 = default) { // check if the model is being exporting already if (!exporting_models.Contains(model)) { exporting_models.Add(model); } else { // model is being exported return false; } 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" }; PostNotification?.Invoke(notification); try { using (var stream = exportStorage.CreateFileSafely(filename)) { success = await ExportToStreamAsync(model, stream, notification, cancellationToken == CancellationToken.None ? notification.CancellationToken : cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) { success = false; } // cleanup if export is failed or canceled. if (!success) { notification.State = ProgressNotificationState.Cancelled; exportStorage.Delete(filename); } else { notification.CompletionText = "Export Complete, Click to open the folder"; notification.CompletionClickAction = () => exportStorage.PresentFileExternally(filename); notification.State = ProgressNotificationState.Completed; } exporting_models.Remove(model); return success; } /// /// Export model to stream. /// /// The model 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 Task ExportToStreamAsync(TModel model, Stream stream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) { Guid id = model.ID; return Task.Run(() => { realmAccess.Run(r => { TModel refetchModel = r.Find(id); ExportToStream(refetchModel, stream, notification, cancellationToken); }); }, cancellationToken).ContinueWith(t => { if (cancellationToken.IsCancellationRequested) { return false; } if (t.IsFaulted) { Logger.Error(t.Exception, "An error occurred while exporting", LoggingTarget.Database); return false; } return true; }, CancellationToken.None); } /// /// Exports model to Stream. /// /// The model 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); } }