mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 02:03:22 +08:00
Merge pull request #21308 from cdwcgt/export
Make model exporting asynchronous
This commit is contained in:
commit
0fa32fed51
@ -1,19 +1,26 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyExporterTest
|
||||
public class LegacyModelExporterTest
|
||||
{
|
||||
private TestLegacyExporter legacyExporter = null!;
|
||||
private TestLegacyModelExporter legacyExporter = null!;
|
||||
private TemporaryNativeStorage storage = null!;
|
||||
|
||||
private const string short_filename = "normal file name";
|
||||
@ -25,15 +32,15 @@ namespace osu.Game.Tests.Database
|
||||
public void SetUp()
|
||||
{
|
||||
storage = new TemporaryNativeStorage("export-storage");
|
||||
legacyExporter = new TestLegacyExporter(storage);
|
||||
legacyExporter = new TestLegacyModelExporter(storage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
var item = new TestModel(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
exportItemAndAssert(item, short_filename);
|
||||
}
|
||||
@ -41,9 +48,9 @@ namespace osu.Game.Tests.Database
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameMultipleTimesTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
var item = new TestModel(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
@ -56,24 +63,24 @@ namespace osu.Game.Tests.Database
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
var item = new TestModel(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
exportItemAndAssert(item, expectedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameMultipleTimesTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
int expectedLength = TestLegacyModelExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
var item = new TestModel(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyModelExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
@ -83,9 +90,12 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
}
|
||||
|
||||
private void exportItemAndAssert(IHasNamedFiles item, string expectedName)
|
||||
private void exportItemAndAssert(TestModel item, string expectedName)
|
||||
{
|
||||
Assert.DoesNotThrow(() => legacyExporter.Export(item));
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
Task.Run(() => legacyExporter.ExportAsync(new RealmLiveUnmanaged<TestModel>(item))).WaitSafely();
|
||||
});
|
||||
Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True);
|
||||
}
|
||||
|
||||
@ -96,30 +106,36 @@ namespace osu.Game.Tests.Database
|
||||
storage.Dispose();
|
||||
}
|
||||
|
||||
private class TestPathInfo : IHasNamedFiles
|
||||
private class TestLegacyModelExporter : LegacyExporter<TestModel>
|
||||
{
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestPathInfo(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
|
||||
private class TestLegacyExporter : LegacyExporter<IHasNamedFiles>
|
||||
{
|
||||
public TestLegacyExporter(Storage storage)
|
||||
public TestLegacyModelExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetExtension() => FileExtension;
|
||||
|
||||
public override void ExportToStream(TestModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FileExtension => ".test";
|
||||
}
|
||||
|
||||
private class TestModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
|
||||
{
|
||||
public Guid ID => Guid.Empty;
|
||||
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestModel(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
@ -120,10 +119,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||
|
||||
string exportFilename = import1.GetDisplayString();
|
||||
|
||||
@ -141,10 +137,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(import1, exportStream);
|
||||
|
||||
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
||||
|
||||
@ -208,7 +201,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportDefaultSkin() => runSkinTest(osu =>
|
||||
public Task TestExportThenImportDefaultSkin() => runSkinTest(async osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
@ -218,30 +211,28 @@ namespace osu.Game.Tests.Skins.IO
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
await skinManager.CurrentSkinInfo.Value.PerformRead(async s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.GetResultSafely().PerformRead(s =>
|
||||
imported.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestExportThenImportClassicSkin() => runSkinTest(osu =>
|
||||
public Task TestExportThenImportClassicSkin() => runSkinTest(async osu =>
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
@ -253,26 +244,24 @@ namespace osu.Game.Tests.Skins.IO
|
||||
|
||||
Guid originalSkinId = skinManager.CurrentSkinInfo.Value.ID;
|
||||
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
await skinManager.CurrentSkinInfo.Value.PerformRead(async s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
await new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportToStreamAsync(skinManager.CurrentSkinInfo.Value, exportStream);
|
||||
|
||||
Assert.Greater(exportStream.Length, 0);
|
||||
});
|
||||
|
||||
var imported = skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
var imported = await skinManager.Import(new ImportTask(exportStream, "exported.osk"));
|
||||
|
||||
imported.GetResultSafely().PerformRead(s =>
|
||||
imported.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(DefaultLegacySkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
@ -42,6 +42,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||
|
||||
private readonly LegacyBeatmapExporter beatmapExporter;
|
||||
|
||||
public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
|
||||
|
||||
public override bool PauseImports
|
||||
@ -76,6 +78,11 @@ namespace osu.Game.Beatmaps
|
||||
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
|
||||
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||
|
||||
beatmapExporter = new LegacyBeatmapExporter(storage)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
|
||||
@ -393,6 +400,8 @@ namespace osu.Game.Beatmaps
|
||||
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
|
||||
beatmapImporter.ImportAsUpdate(notification, importTask, original);
|
||||
|
||||
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||
|
||||
private void updateHashAndMarkDirty(BeatmapSetInfo setInfo)
|
||||
{
|
||||
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
|
||||
|
69
osu.Game/Database/LegacyArchiveExporter.cs
Normal file
69
osu.Game/Database/LegacyArchiveExporter.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Logger = osu.Framework.Logging.Logger;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the common scenario of exporting a model to a zip-based archive, usually with a custom file extension.
|
||||
/// </summary>
|
||||
public abstract class LegacyArchiveExporter<TModel> : LegacyExporter<TModel>
|
||||
where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
|
||||
{
|
||||
protected LegacyArchiveExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate)))
|
||||
{
|
||||
int i = 0;
|
||||
int fileCount = model.Files.Count();
|
||||
bool anyFileMissing = false;
|
||||
|
||||
foreach (var file in model.Files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath()))
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
Logger.Log($"File {file.Filename} is missing in local storage and will not be included in the export", LoggingTarget.Database);
|
||||
anyFileMissing = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.Write(file.Filename, stream);
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
if (notification != null)
|
||||
{
|
||||
notification.Progress = (float)i / fileCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyFileMissing)
|
||||
{
|
||||
Logger.Log("Some files are missing in local storage and will not be included in the export", LoggingTarget.Database, LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,18 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class LegacyBeatmapExporter : LegacyExporter<BeatmapSetInfo>
|
||||
public class LegacyBeatmapExporter : LegacyArchiveExporter<BeatmapSetInfo>
|
||||
{
|
||||
protected override string FileExtension => ".osz";
|
||||
|
||||
public LegacyBeatmapExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FileExtension => @".osz";
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,25 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
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 SharpCompress.Archives.Zip;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A class which handles exporting legacy user data of a single type from osu-stable.
|
||||
/// Handles exporting models to files for sharing / consumption outside the game.
|
||||
/// </summary>
|
||||
public abstract class LegacyExporter<TModel>
|
||||
where TModel : class, IHasNamedFiles
|
||||
where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Max length of filename (including extension).
|
||||
@ -39,55 +40,93 @@ namespace osu.Game.Database
|
||||
protected abstract string FileExtension { get; }
|
||||
|
||||
protected readonly Storage UserFileStorage;
|
||||
|
||||
private readonly Storage exportStorage;
|
||||
|
||||
public Action<Notification>? PostNotification { get; set; }
|
||||
|
||||
protected LegacyExporter(Storage storage)
|
||||
{
|
||||
exportStorage = storage.GetStorageForDirectory(@"exports");
|
||||
UserFileStorage = storage.GetStorageForDirectory(@"files");
|
||||
}
|
||||
|
||||
/// <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();
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item to a legacy (.zip based) package.
|
||||
/// Exports a model to the default export location.
|
||||
/// This will create a notification tracking the progress of the export, visible to the user.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to export.</param>
|
||||
public void Export(TModel item)
|
||||
/// <param name="model">The model to export.</param>
|
||||
/// <param name="cancellationToken">A cancellation token.</param>
|
||||
public async Task ExportAsync(Live<TModel> model, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string itemFilename = GetFilename(item).GetValidFilename();
|
||||
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<string> existingExports =
|
||||
exportStorage
|
||||
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
|
||||
.Concat(exportStorage.GetDirectories(string.Empty));
|
||||
IEnumerable<string> existingExports = exportStorage
|
||||
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
|
||||
.Concat(exportStorage.GetDirectories(string.Empty));
|
||||
|
||||
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
|
||||
|
||||
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||
ExportModelTo(item, stream);
|
||||
ProgressNotification notification = new ProgressNotification
|
||||
{
|
||||
State = ProgressNotificationState.Active,
|
||||
Text = $"Exporting {itemFilename}...",
|
||||
};
|
||||
|
||||
exportStorage.PresentFileExternally(filename);
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item to the given output stream.
|
||||
/// Exports a model to a provided stream.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to export.</param>
|
||||
/// <param name="model">The model to export.</param>
|
||||
/// <param name="outputStream">The output stream to export to.</param>
|
||||
public virtual void ExportModelTo(TModel model, Stream outputStream)
|
||||
{
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
foreach (var file in model.Files)
|
||||
archive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath()));
|
||||
/// <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);
|
||||
|
||||
archive.SaveTo(outputStream);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Exports a model to a provided stream.
|
||||
/// </summary>
|
||||
/// <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 abstract void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class LegacyScoreExporter : LegacyExporter<ScoreInfo>
|
||||
{
|
||||
protected override string FileExtension => ".osr";
|
||||
|
||||
public LegacyScoreExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
@ -28,7 +26,9 @@ namespace osu.Game.Database
|
||||
return filename;
|
||||
}
|
||||
|
||||
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||
protected override string FileExtension => @".osr";
|
||||
|
||||
public override void ExportToStream(ScoreInfo model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = model.Files.SingleOrDefault();
|
||||
if (file == null)
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -22,7 +20,7 @@ namespace osu.Game.Database
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return storage.GetFiles(ImportFromStablePath)
|
||||
.Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
.Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p).Equals(ext, StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(path => storage.GetFullPath(path));
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,18 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class LegacySkinExporter : LegacyExporter<SkinInfo>
|
||||
public class LegacySkinExporter : LegacyArchiveExporter<SkinInfo>
|
||||
{
|
||||
protected override string FileExtension => ".osk";
|
||||
|
||||
public LegacySkinExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FileExtension => @".osk";
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Database
|
||||
|
@ -17,8 +17,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -66,18 +64,18 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private List<ScoreComponentLabel> statisticsLabels;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[Resolved(canBeNull: true)]
|
||||
private SongSelect songSelect { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
public ITooltip<ScoreInfo> GetCustomTooltip() => new LeaderboardScoreTooltip();
|
||||
public virtual ScoreInfo TooltipContent => Score;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true)
|
||||
{
|
||||
Score = score;
|
||||
@ -90,7 +88,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
|
||||
private void load(IAPIProvider api, OsuColour colour)
|
||||
{
|
||||
var user = Score.User;
|
||||
|
||||
@ -427,7 +425,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
if (Score.Files.Count > 0)
|
||||
{
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
|
||||
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
@ -139,9 +138,6 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
private Bindable<Skin> currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -163,7 +159,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
try
|
||||
{
|
||||
currentSkin.Value.SkinInfo.PerformRead(s => new LegacySkinExporter(storage).Export(s));
|
||||
skins.ExportCurrentSkin();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -27,6 +27,7 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
private readonly OsuConfigManager configManager;
|
||||
private readonly ScoreImporter scoreImporter;
|
||||
private readonly LegacyScoreExporter scoreExporter;
|
||||
|
||||
public override bool PauseImports
|
||||
{
|
||||
@ -48,6 +49,11 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
|
||||
scoreExporter = new LegacyScoreExporter(storage)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
}
|
||||
|
||||
public Score GetScore(ScoreInfo score) => scoreImporter.GetScore(score);
|
||||
@ -187,6 +193,8 @@ namespace osu.Game.Scoring
|
||||
|
||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => scoreImporter.Import(notification, tasks);
|
||||
|
||||
public Task Export(ScoreInfo score) => scoreExporter.ExportAsync(score.ToLive(Realm));
|
||||
|
||||
public Task<Live<ScoreInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
|
||||
|
||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
|
@ -20,7 +20,6 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
@ -29,7 +28,6 @@ using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
@ -88,9 +86,6 @@ namespace osu.Game.Screens.Edit
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
@ -983,7 +978,7 @@ namespace osu.Game.Screens.Edit
|
||||
private void exportBeatmap()
|
||||
{
|
||||
Save();
|
||||
new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo);
|
||||
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -58,6 +58,8 @@ namespace osu.Game.Skinning
|
||||
|
||||
private readonly SkinImporter skinImporter;
|
||||
|
||||
private readonly LegacySkinExporter skinExporter;
|
||||
|
||||
private readonly IResourceStore<byte[]> userFiles;
|
||||
|
||||
private Skin argonSkin { get; }
|
||||
@ -120,6 +122,11 @@ namespace osu.Game.Skinning
|
||||
|
||||
SourceChanged?.Invoke();
|
||||
};
|
||||
|
||||
skinExporter = new LegacySkinExporter(storage)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
}
|
||||
|
||||
public void SelectRandomSkin()
|
||||
@ -298,6 +305,10 @@ namespace osu.Game.Skinning
|
||||
public Task<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
skinImporter.Import(task, parameters, cancellationToken);
|
||||
|
||||
public Task ExportCurrentSkin() => ExportSkin(CurrentSkinInfo.Value);
|
||||
|
||||
public Task ExportSkin(Live<SkinInfo> skin) => skinExporter.ExportAsync(skin);
|
||||
|
||||
#endregion
|
||||
|
||||
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)
|
||||
|
Loading…
Reference in New Issue
Block a user