// 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.Tasks; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Overlays.Notifications; using osu.Game.Utils; using Realms; using SharpCompress.Common; using SharpCompress.Writers; using SharpCompress.Writers.Zip; 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 RealmAccess RealmAccess; private string filename = string.Empty; public Action? PostNotification { get; set; } 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. /// public async Task ExportAsync(TModel model) { string itemFilename = model.GetDisplayString().GetValidFilename(); IEnumerable existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}"); filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); bool success; using (var stream = exportStorage.CreateFileSafely(filename)) { success = await ExportToStreamAsync(model, stream); } if (!success) { exportStorage.Delete(filename); } } /// /// Export model to stream. /// /// The medel which have . /// The stream to export. /// Whether the export was successful public async Task ExportToStreamAsync(TModel model, Stream stream) { ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active, Text = "Exporting...", CompletionText = "Export completed" }; notification.CompletionClickAction += () => exportStorage.PresentFileExternally(filename); PostNotification?.Invoke(notification); Guid id = model.ID; return await Task.Run(() => { RealmAccess.Run(r => { TModel refetchModel = r.Find(id); ExportToStream(refetchModel, stream, notification); }); }, notification.CancellationToken).ContinueWith(t => { if (t.IsCanceled) { return false; } if (t.IsFaulted) { notification.State = ProgressNotificationState.Cancelled; Logger.Error(t.Exception, "An error occurred while exporting"); return false; } notification.CompletionText = "Export Complete, Click to open the folder"; notification.State = ProgressNotificationState.Completed; return true; }); } /// /// 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 protected abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification notification); } public abstract class LegacyArchiveExporter : LegacyModelExporter where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey { protected LegacyArchiveExporter(Storage storage, RealmAccess realm) : base(storage, realm) { } protected override void ExportToStream(TModel model, Stream outputStream, ProgressNotification notification) => exportZipArchive(model, outputStream, notification); /// /// Exports an item to Stream as a legacy (.zip based) package. /// /// The item to export. /// The output stream to export to. /// The notification will displayed to the user private void exportZipArchive(TModel model, Stream outputStream, ProgressNotification notification) { try { using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate))) { float i = 0; foreach (var file in model.Files) { notification.CancellationToken.ThrowIfCancellationRequested(); writer.Write(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath())); i++; notification.Progress = i / model.Files.Count(); notification.Text = $"Exporting... ({i}/{model.Files.Count()})"; } } } catch (OperationCanceledException) { Logger.Log("Export operat canceled"); throw; } } } }