// 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.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 LegacyExporter where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey { /// /// Max length of filename (including extension). /// /// /// /// The filename limit for most OSs is 255. This actual usable length is smaller because adds an additional "_" to the end of the path. /// /// /// For more information see file specification syntax, file systems limitations /// /// public const int MAX_FILENAME_LENGTH = 255 - (32 + 4 + 2 + 5); //max path - (Guid + Guid "D" format chars + Storage.CreateFileSafely chars + account for ' (99)' suffix) /// /// 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(); 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 protected LegacyExporter(Storage storage) { exportStorage = storage.GetStorageForDirectory(@"exports"); UserFileStorage = storage.GetStorageForDirectory(@"files"); } /// /// Export the model to default folder. /// /// The model should export. /// Realm that convert model to Live. /// /// The Cancellation token that can cancel the exporting. /// If specified CancellationToken, then use it. Otherwise use PostNotification's CancellationToken. /// /// public Task ExportAsync(TModel model, RealmAccess realm, CancellationToken cancellationToken = default) { return ExportAsync(model.ToLive(realm), cancellationToken); } /// /// 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(Live 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 = model.PerformRead(s => GetFilename(s).GetValidFilename()); if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length) itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length); IEnumerable existingExports = exportStorage .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}") .Concat(exportStorage.GetDirectories(string.Empty)); string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); bool success; ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, Text = $"Exporting {itemFilename}...", }; 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 { notification.State = ProgressNotificationState.Cancelled; throw; } finally { // Determines whether to export repeatedly, so he must be removed from the list at the end whether there is a error. exporting_models.Remove(model); } // cleanup if export is failed or canceled. if (!success) { notification.State = ProgressNotificationState.Cancelled; exportStorage.Delete(filename); } else { notification.CompletionText = $"Exported {itemFilename}! Click to view."; notification.CompletionClickAction = () => exportStorage.PresentFileExternally(filename); notification.State = ProgressNotificationState.Completed; } 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(Live model, Stream stream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) => Task.Run(() => { model.PerformRead(s => ExportToStream(s, stream, notification, cancellationToken)); }, cancellationToken); /// /// 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); } }