// 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 { /// /// Handles exporting models to files for sharing / consumption outside the game. /// 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 readonly Storage UserFileStorage; private readonly Storage exportStorage; public Action? PostNotification { get; set; } protected LegacyExporter(Storage storage) { exportStorage = storage.GetStorageForDirectory(@"exports"); UserFileStorage = storage.GetStorageForDirectory(@"files"); } /// /// Returns the baseline name of the file to which the will be exported. /// /// /// The name of the file will be run through to eliminate characters /// which are not permitted by various filesystems. /// /// The item being exported. protected virtual string GetFilename(TModel item) => item.GetDisplayString(); /// /// Exports a model to the default export location. /// This will create a notification tracking the progress of the export, visible to the user. /// /// The model to export. /// A cancellation token. public async Task ExportAsync(Live model, CancellationToken cancellationToken = default) { 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}"); ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, Text = $"Exporting {itemFilename}...", }; PostNotification?.Invoke(notification); using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, notification.CancellationToken); try { using (var stream = exportStorage.CreateFileSafely(filename)) { await ExportToStreamAsync(model, stream, notification, linkedSource.Token).ConfigureAwait(false); } } catch { notification.State = ProgressNotificationState.Cancelled; // cleanup if export is failed or canceled. exportStorage.Delete(filename); throw; } notification.CompletionText = $"Exported {itemFilename}! Click to view."; notification.CompletionClickAction = () => exportStorage.PresentFileExternally(filename); notification.State = ProgressNotificationState.Completed; } /// /// Exports a model to a provided stream. /// /// The model to export. /// The output stream to export to. /// An optional notification to be updated with export progress. /// A cancellation token. public Task ExportToStreamAsync(Live model, Stream outputStream, ProgressNotification? notification = null, CancellationToken cancellationToken = default) => Task.Run(() => { model.PerformRead(s => ExportToStream(s, outputStream, notification, cancellationToken)); }, cancellationToken); /// /// Exports a model to a provided stream. /// /// The model to export. /// The output stream to export to. /// An optional notification to be updated with export progress. /// A cancellation token. public abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default); } }