1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 20:43:21 +08:00

Merge pull request #15794 from peppy/realm-integration/stable-export-flow

Split out legacy model export logic into `LegacyModelExporter` classes
This commit is contained in:
Dan Balasescu 2021-11-25 20:20:18 +09:00 committed by GitHub
commit d7a960212f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 198 additions and 128 deletions

View File

@ -225,16 +225,6 @@ namespace osu.Game.Beatmaps
remove => beatmapModelManager.ItemRemoved -= value;
}
public void Export(BeatmapSetInfo item)
{
beatmapModelManager.Export(item);
}
public void ExportModelTo(BeatmapSetInfo model, Stream outputStream)
{
beatmapModelManager.ExportModelTo(model, outputStream);
}
public void Update(BeatmapSetInfo item)
{
beatmapModelManager.Update(item);

View File

@ -212,7 +212,7 @@ namespace osu.Game.Beatmaps
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
// metadata may have changed; update the path with the standard format.
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu");
beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();

View File

@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps
IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata();
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
#endregion
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive.
/// </summary>
public interface IBeatmapSetInfo : IHasOnlineID<int>, IEquatable<IBeatmapSetInfo>
public interface IBeatmapSetInfo : IHasOnlineID<int>, IEquatable<IBeatmapSetInfo>, IHasNamedFiles
{
/// <summary>
/// The date when this beatmap was imported.
@ -29,11 +29,6 @@ namespace osu.Game.Beatmaps
/// </summary>
IEnumerable<IBeatmapInfo> Beatmaps { get; }
/// <summary>
/// All files used by this set.
/// </summary>
IEnumerable<INamedFileUsage> Files { get; }
/// <summary>
/// The maximum star difficulty of all beatmaps in this set.
/// </summary>

View File

@ -20,7 +20,6 @@ using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using SharpCompress.Archives.Zip;
namespace osu.Game.Database
{
@ -82,8 +81,6 @@ namespace osu.Game.Database
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc;
private readonly Storage exportStorage;
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore, IIpcHost importHost = null)
{
ContextFactory = contextFactory;
@ -92,8 +89,6 @@ namespace osu.Game.Database
ModelStore.ItemUpdated += item => handleEvent(() => ItemUpdated?.Invoke(item));
ModelStore.ItemRemoved += item => handleEvent(() => ItemRemoved?.Invoke(item));
exportStorage = storage.GetStorageForDirectory(@"exports");
Files = new FileStore(contextFactory, storage);
if (importHost != null)
@ -452,41 +447,6 @@ namespace osu.Game.Database
return item.ToEntityFrameworkLive();
}, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false);
/// <summary>
/// Exports an item to a legacy (.zip based) package.
/// </summary>
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID);
if (retrievedItem == null)
throw new ArgumentException(@"Specified model could not be found", nameof(item));
string filename = $"{GetValidFilename(item.ToString())}{HandledExtensions.First()}";
using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))
ExportModelTo(retrievedItem, stream);
exportStorage.PresentFileExternally(filename);
}
/// <summary>
/// Exports an item to the given output stream.
/// </summary>
/// <param name="model">The item 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, Files.Storage.GetStream(file.FileInfo.GetStoragePath()));
archive.SaveTo(outputStream);
}
}
/// <summary>
/// Replace an existing file with a new version.
/// </summary>
@ -875,18 +835,5 @@ namespace osu.Game.Database
// this doesn't follow the SHA2 hashing schema intentionally, so such entries on the data store can be identified.
return Guid.NewGuid().ToString();
}
private readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars()
// Backslash is added to avoid issues when exporting to zip.
// See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
.Append('\\')
.ToArray();
protected string GetValidFilename(string filename)
{
foreach (char c in invalidFilenameCharacters)
filename = filename.Replace(c, '_');
return filename;
}
}
}

View File

@ -0,0 +1,15 @@
// 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.Collections.Generic;
namespace osu.Game.Database
{
public interface IHasNamedFiles
{
/// <summary>
/// All files used by this model.
/// </summary>
IEnumerable<INamedFileUsage> Files { get; }
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace osu.Game.Database
{
@ -24,19 +23,6 @@ namespace osu.Game.Database
/// </summary>
event Action<TModel> ItemRemoved;
/// <summary>
/// Exports an item to a legacy (.zip based) package.
/// </summary>
/// <param name="item">The item to export.</param>
void Export(TModel item);
/// <summary>
/// Exports an item to the given output stream.
/// </summary>
/// <param name="model">The item to export.</param>
/// <param name="outputStream">The output stream to export to.</param>
void ExportModelTo(TModel model, Stream outputStream);
/// <summary>
/// Perform an update of the specified item.
/// TODO: Support file additions/removals.

View File

@ -0,0 +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.
using osu.Framework.Platform;
using osu.Game.Beatmaps;
namespace osu.Game.Database
{
public class LegacyBeatmapExporter : LegacyExporter<BeatmapSetInfo>
{
protected override string FileExtension => ".osz";
public LegacyBeatmapExporter(Storage storage)
: base(storage)
{
}
}
}

View File

@ -0,0 +1,62 @@
// 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 osu.Framework.Platform;
using osu.Game.Extensions;
using SharpCompress.Archives.Zip;
namespace osu.Game.Database
{
/// <summary>
/// A class which handles exporting legacy user data of a single type from osu-stable.
/// </summary>
public abstract class LegacyExporter<TModel>
where TModel : class, IHasNamedFiles
{
/// <summary>
/// The file extension for exports (including the leading '.').
/// </summary>
protected abstract string FileExtension { get; }
protected readonly Storage UserFileStorage;
private readonly Storage exportStorage;
protected LegacyExporter(Storage storage)
{
exportStorage = storage.GetStorageForDirectory(@"exports");
UserFileStorage = storage.GetStorageForDirectory(@"files");
}
/// <summary>
/// Exports an item to a legacy (.zip based) package.
/// </summary>
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
string filename = $"{item.ToString().GetValidArchiveContentFilename()}{FileExtension}";
using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))
ExportModelTo(item, stream);
exportStorage.PresentFileExternally(filename);
}
/// <summary>
/// Exports an item to the given output stream.
/// </summary>
/// <param name="model">The item 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()));
archive.SaveTo(outputStream);
}
}
}
}

View File

@ -0,0 +1,31 @@
// 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 osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Scoring;
namespace osu.Game.Database
{
public class LegacyScoreExporter : LegacyExporter<ScoreInfo>
{
protected override string FileExtension => ".osr";
public LegacyScoreExporter(Storage storage)
: base(storage)
{
}
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();
if (file == null)
return;
using (var inputStream = UserFileStorage.GetStream(file.FileInfo.GetStoragePath()))
inputStream.CopyTo(outputStream);
}
}
}

View File

@ -0,0 +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.
using osu.Framework.Platform;
using osu.Game.Skinning;
namespace osu.Game.Database
{
public class LegacySkinExporter : LegacyExporter<SkinInfo>
{
protected override string FileExtension => ".osk";
public LegacySkinExporter(Storage storage)
: base(storage)
{
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
@ -124,5 +125,21 @@ namespace osu.Game.Extensions
return instance.OnlineID.Equals(other.OnlineID);
}
private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars()
// Backslash is added to avoid issues when exporting to zip.
// See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
.Append('\\')
.ToArray();
/// <summary>
/// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories.
/// </summary>
public static string GetValidArchiveContentFilename(this string filename)
{
foreach (char c in invalid_filename_characters)
filename = filename.Replace(c, '_');
return filename;
}
}
}

View File

@ -76,7 +76,6 @@ namespace osu.Game.Models
public bool Equals(IBeatmapSetInfo? other) => other is RealmBeatmapSet b && Equals(b);
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -136,7 +136,7 @@ namespace osu.Game.Online.API.Requests.Responses
IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => metadata;
DateTimeOffset IBeatmapSetInfo.DateAdded => throw new NotImplementedException();
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => throw new NotImplementedException();
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => throw new NotImplementedException();
double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException();
double IBeatmapSetInfo.MaxLength => throw new NotImplementedException();
double IBeatmapSetInfo.MaxBPM => BPM;

View File

@ -8,6 +8,7 @@ using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@ -147,6 +148,7 @@ namespace osu.Game.Online.API.Requests.Responses
}
public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID };
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => throw new NotImplementedException();
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
}

View File

@ -14,6 +14,8 @@ 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.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -66,6 +68,9 @@ namespace osu.Game.Online.Leaderboards
[Resolved]
private ScoreManager scoreManager { get; set; }
[Resolved]
private Storage storage { get; set; }
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
{
Score = score;
@ -395,7 +400,7 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.Files.Count > 0)
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
if (Score.ID != 0)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));

View File

@ -11,7 +11,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Skinning;
@ -167,6 +169,9 @@ namespace osu.Game.Overlays.Settings.Sections
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private Storage storage { get; set; }
private Bindable<Skin> currentSkin;
[BackgroundDependencyLoader]
@ -183,7 +188,7 @@ namespace osu.Game.Overlays.Settings.Sections
{
try
{
skins.Export(currentSkin.Value.SkinInfo);
new LegacySkinExporter(storage).Export(currentSkin.Value.SkinInfo);
}
catch (Exception e)
{

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Scoring
{
public interface IScoreInfo : IHasOnlineID<long>
public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles
{
APIUser User { get; }

View File

@ -7,7 +7,7 @@ using osu.Game.IO;
namespace osu.Game.Scoring
{
public class ScoreFileInfo : INamedFileInfo, IHasPrimaryKey
public class ScoreFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage
{
public int ID { get; set; }
@ -17,5 +17,7 @@ namespace osu.Game.Scoring
[Required]
public string Filename { get; set; }
IFileInfo INamedFileUsage.File => FileInfo;
}
}

View File

@ -257,5 +257,7 @@ namespace osu.Game.Scoring
bool IScoreInfo.HasReplay => Files.Any();
#endregion
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
@ -262,16 +261,6 @@ namespace osu.Game.Scoring
remove => scoreModelManager.ItemRemoved -= value;
}
public void Export(ScoreInfo item)
{
scoreModelManager.Export(item);
}
public void ExportModelTo(ScoreInfo model, Stream outputStream)
{
scoreModelManager.ExportModelTo(model, outputStream);
}
public void Update(ScoreInfo item)
{
scoreModelManager.Update(item);

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
@ -13,7 +12,6 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy;
@ -69,15 +67,5 @@ namespace osu.Game.Scoring
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items)
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();
if (file == null)
return;
using (var inputStream = Files.Storage.GetStream(file.FileInfo.GetStoragePath()))
inputStream.CopyTo(outputStream);
}
}
}

View File

@ -16,9 +16,11 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
@ -63,6 +65,9 @@ namespace osu.Game.Screens.Edit
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private Storage storage { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
@ -753,7 +758,7 @@ namespace osu.Game.Screens.Edit
private void exportBeatmap()
{
Save();
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo);
}
private void updateLastSavedHash()

View File

@ -7,7 +7,7 @@ using osu.Game.IO;
namespace osu.Game.Skinning
{
public class SkinFileInfo : INamedFileInfo, IHasPrimaryKey
public class SkinFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage
{
public int ID { get; set; }
@ -19,5 +19,7 @@ namespace osu.Game.Skinning
[Required]
public string Filename { get; set; }
IFileInfo INamedFileUsage.File => FileInfo;
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.IO;
namespace osu.Game.Skinning
{
public class SkinInfo : IHasFiles<SkinFileInfo>, IEquatable<SkinInfo>, IHasPrimaryKey, ISoftDelete
public class SkinInfo : IHasFiles<SkinFileInfo>, IEquatable<SkinInfo>, IHasPrimaryKey, ISoftDelete, IHasNamedFiles
{
internal const int DEFAULT_SKIN = 0;
internal const int CLASSIC_SKIN = -1;
@ -55,5 +55,7 @@ namespace osu.Game.Skinning
string author = Creator == null ? string.Empty : $"({Creator})";
return $"{Name} {author}".Trim();
}
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
}
}

View File

@ -301,16 +301,6 @@ namespace osu.Game.Skinning
remove => skinModelManager.ItemRemoved -= value;
}
public void Export(SkinInfo item)
{
skinModelManager.Export(item);
}
public void ExportModelTo(SkinInfo model, Stream outputStream)
{
skinModelManager.ExportModelTo(model, outputStream);
}
public void Update(SkinInfo item)
{
skinModelManager.Update(item);