// 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.Archives.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 bool canCancel = true; 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}"); using (var stream = exportStorage.CreateFileSafely(filename)) { await ExportToStreamAsync(model, stream); } } /// /// Export model to stream. /// /// The medel which have . /// The stream to export. /// 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); notification.CancelRequested += () => canCancel; PostNotification?.Invoke(notification); canCancel = true; Guid id = model.ID; await Task.Run(() => { RealmAccess.Run(r => { TModel refetchModel = r.Find(id); ExportToStream(refetchModel, stream, notification); }); }).ContinueWith(t => { if (t.IsFaulted) { notification.State = ProgressNotificationState.Cancelled; Logger.Error(t.Exception, "An error occurred while exporting"); return; } if (notification.CancellationToken.IsCancellationRequested) { return; } notification.CompletionText = "Export Complete, Click to open the folder"; notification.State = ProgressNotificationState.Completed; }); } /// /// 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 virtual 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) { using (var archive = ZipArchive.Create()) { float i = 0; foreach (var file in model.Files) { if (notification.CancellationToken.IsCancellationRequested) return; archive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath())); i++; notification.Progress = i / model.Files.Count(); notification.Text = $"Exporting... ({i}/{model.Files.Count()})"; } notification.Text = "Saving Zip Archive..."; canCancel = false; archive.SaveTo(outputStream); } } } }