2022-11-17 22:38:24 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
2022-12-09 22:41:07 +08:00
using System.Collections.Generic ;
2022-11-19 00:02:35 +08:00
using System.IO ;
2022-11-17 22:38:24 +08:00
using System.Linq ;
2023-02-17 20:52:10 +08:00
using System.Threading ;
2022-11-17 22:38:24 +08:00
using System.Threading.Tasks ;
using osu.Framework.Platform ;
using osu.Game.Extensions ;
using osu.Game.Overlays.Notifications ;
2022-12-09 22:41:07 +08:00
using osu.Game.Utils ;
2022-11-17 22:38:24 +08:00
using Realms ;
namespace osu.Game.Database
{
/// <summary>
2023-05-06 22:10:09 +08:00
/// Handles exporting models to files for sharing / consumption outside the game.
2022-11-17 22:38:24 +08:00
/// </summary>
2023-05-05 15:19:09 +08:00
public abstract class LegacyExporter < TModel >
2022-11-21 17:58:01 +08:00
where TModel : RealmObject , IHasNamedFiles , IHasGuidPrimaryKey
2022-11-17 22:38:24 +08:00
{
2023-04-09 13:47:53 +08:00
/// <summary>
/// Max length of filename (including extension).
/// </summary>
/// <remarks>
/// <para>
/// The filename limit for most OSs is 255. This actual usable length is smaller because <see cref="Storage.CreateFileSafely(string)"/> adds an additional "_<see cref="Guid"/>" to the end of the path.
/// </para>
/// <para>
/// For more information see <see href="https://www.ibm.com/docs/en/spectrum-protect/8.1.9?topic=parameters-file-specification-syntax">file specification syntax</see>, <seealso href="https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits">file systems limitations</seealso>
/// </para>
/// </remarks>
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)
2022-11-17 22:38:24 +08:00
/// <summary>
/// The file extension for exports (including the leading '.').
/// </summary>
protected abstract string FileExtension { get ; }
2023-05-07 01:26:51 +08:00
protected readonly Storage UserFileStorage ;
2022-12-09 22:57:03 +08:00
private readonly Storage exportStorage ;
2022-11-17 22:38:24 +08:00
2022-12-11 17:30:24 +08:00
public Action < Notification > ? PostNotification { get ; set ; }
2022-12-09 22:57:03 +08:00
2023-05-05 15:19:09 +08:00
protected LegacyExporter ( Storage storage )
2022-11-17 22:38:24 +08:00
{
2022-12-09 22:57:03 +08:00
exportStorage = storage . GetStorageForDirectory ( @"exports" ) ;
2022-11-17 22:38:24 +08:00
UserFileStorage = storage . GetStorageForDirectory ( @"files" ) ;
2023-04-09 21:15:00 +08:00
}
2023-05-07 01:29:08 +08:00
/// <summary>
/// Returns the baseline name of the file to which the <paramref name="item"/> will be exported.
/// </summary>
/// <remarks>
/// The name of the file will be run through <see cref="ModelExtensions.GetValidFilename"/> to eliminate characters
/// which are not permitted by various filesystems.
/// </remarks>
/// <param name="item">The item being exported.</param>
protected virtual string GetFilename ( TModel item ) = > item . GetDisplayString ( ) ;
2022-11-21 18:04:05 +08:00
/// <summary>
2023-05-06 22:53:35 +08:00
/// Exports a model to the default export location.
/// This will create a notification tracking the progress of the export, visible to the user.
2022-11-21 18:04:05 +08:00
/// </summary>
2023-05-06 22:53:35 +08:00
/// <param name="model">The model to export.</param>
/// <param name="cancellationToken">A cancellation token.</param>
2023-05-05 20:04:13 +08:00
public async Task ExportAsync ( Live < TModel > model , CancellationToken cancellationToken = default )
2022-11-17 22:38:24 +08:00
{
2023-04-09 21:15:00 +08:00
string itemFilename = model . PerformRead ( s = > GetFilename ( s ) . GetValidFilename ( ) ) ;
2023-04-09 13:47:53 +08:00
if ( itemFilename . Length > MAX_FILENAME_LENGTH - FileExtension . Length )
itemFilename = itemFilename . Remove ( MAX_FILENAME_LENGTH - FileExtension . Length ) ;
2023-05-05 20:04:13 +08:00
IEnumerable < string > existingExports = exportStorage
. GetFiles ( string . Empty , $"{itemFilename}*{FileExtension}" )
. Concat ( exportStorage . GetDirectories ( string . Empty ) ) ;
2023-02-17 21:23:50 +08:00
string filename = NamingUtils . GetNextBestFilename ( existingExports , $"{itemFilename}{FileExtension}" ) ;
2022-12-09 22:57:03 +08:00
2023-02-17 20:52:10 +08:00
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState . Active ,
2023-05-05 15:53:27 +08:00
Text = $"Exporting {itemFilename}..." ,
2023-02-17 20:52:10 +08:00
} ;
2023-05-05 20:04:13 +08:00
2023-02-17 20:52:10 +08:00
PostNotification ? . Invoke ( notification ) ;
2023-05-05 20:08:01 +08:00
using var linkedSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , notification . CancellationToken ) ;
2023-02-19 00:45:09 +08:00
try
2022-12-09 23:43:03 +08:00
{
2023-02-19 00:45:09 +08:00
using ( var stream = exportStorage . CreateFileSafely ( filename ) )
{
2023-05-05 20:08:01 +08:00
await ExportToStreamAsync ( model , stream , notification , linkedSource . Token ) . ConfigureAwait ( false ) ;
2023-02-19 00:45:09 +08:00
}
}
2023-04-09 14:09:18 +08:00
catch
2023-02-19 00:45:09 +08:00
{
2023-04-09 14:09:18 +08:00
notification . State = ProgressNotificationState . Cancelled ;
2023-02-19 01:09:59 +08:00
2023-05-05 20:04:13 +08:00
// cleanup if export is failed or canceled.
2023-02-26 14:28:24 +08:00
exportStorage . Delete ( filename ) ;
2023-05-05 20:04:13 +08:00
throw ;
2023-02-26 14:28:24 +08:00
}
2023-05-05 20:04:13 +08:00
notification . CompletionText = $"Exported {itemFilename}! Click to view." ;
notification . CompletionClickAction = ( ) = > exportStorage . PresentFileExternally ( filename ) ;
notification . State = ProgressNotificationState . Completed ;
2022-11-21 17:58:01 +08:00
}
2022-11-21 18:04:05 +08:00
/// <summary>
2023-05-06 22:53:35 +08:00
/// Exports a model to a provided stream.
2022-11-21 18:04:05 +08:00
/// </summary>
2023-05-06 22:53:35 +08:00
/// <param name="model">The model to export.</param>
/// <param name="outputStream">The output stream to export to.</param>
/// <param name="notification">An optional notification to be updated with export progress.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public Task ExportToStreamAsync ( Live < TModel > model , Stream outputStream , ProgressNotification ? notification = null , CancellationToken cancellationToken = default ) = >
Task . Run ( ( ) = > { model . PerformRead ( s = > ExportToStream ( s , outputStream , notification , cancellationToken ) ) ; } , cancellationToken ) ;
2022-11-19 00:02:35 +08:00
2022-12-11 15:55:44 +08:00
/// <summary>
2023-05-06 22:53:35 +08:00
/// Exports a model to a provided stream.
2022-12-11 15:55:44 +08:00
/// </summary>
2023-02-21 19:54:06 +08:00
/// <param name="model">The model to export.</param>
2022-12-11 15:55:44 +08:00
/// <param name="outputStream">The output stream to export to.</param>
2023-05-06 22:53:35 +08:00
/// <param name="notification">An optional notification to be updated with export progress.</param>
/// <param name="cancellationToken">A cancellation token.</param>
2023-05-05 20:28:43 +08:00
public abstract void ExportToStream ( TModel model , Stream outputStream , ProgressNotification ? notification , CancellationToken cancellationToken = default ) ;
2022-12-15 20:39:48 +08:00
}
2022-11-17 22:38:24 +08:00
}