Move import logic to shared implementation

This commit is contained in:
Dean Herbert
2018-02-14 20:26:49 +09:00
parent af61a524b5
commit e0d28564d0
16 changed files with 340 additions and 209 deletions

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Ionic.Zip;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Extensions;
using osu.Framework.Logging;
@ -16,8 +15,6 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.IO;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Notifications;
@ -28,7 +25,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary>
public partial class BeatmapManager
public partial class BeatmapManager : ArchiveModelImportManager<BeatmapSetInfo, BeatmapSetFileInfo>
{
/// <summary>
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
@ -60,9 +57,7 @@ namespace osu.Game.Beatmaps
/// </summary>
public WorkingBeatmap DefaultBeatmap { private get; set; }
private readonly IDatabaseContextFactory contextFactory;
private readonly FileStore files;
public override string[] HandledExtensions => new[] { ".osz" };
private readonly RulesetStore rulesets;
@ -72,142 +67,58 @@ namespace osu.Game.Beatmaps
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private BeatmapIPCChannel ipc;
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
public Action<Notification> PostNotification { private get; set; }
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
{
this.contextFactory = contextFactory;
beatmaps = new BeatmapStore(contextFactory);
beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
files = new FileStore(contextFactory, storage);
this.rulesets = rulesets;
this.api = api;
if (importHost != null)
ipc = new BeatmapIPCChannel(importHost, this);
beatmaps.Cleanup();
}
/// <summary>
/// Import one or more <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
/// This will post notifications tracking progress.
/// </summary>
/// <param name="paths">One or more beatmap locations on disk.</param>
public List<BeatmapSetInfo> Import(params string[] paths)
protected override void Populate(BeatmapSetInfo model, ArchiveReader archive)
{
var notification = new ProgressNotification
{
Text = "Beatmap import is initialising...",
CompletionText = "Import successful!",
Progress = 0,
State = ProgressNotificationState.Active,
};
model.Beatmaps = createBeatmapDifficulties(archive);
PostNotification?.Invoke(notification);
List<BeatmapSetInfo> imported = new List<BeatmapSetInfo>();
int i = 0;
foreach (string path in paths)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return imported;
try
{
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
using (ArchiveReader reader = getReaderFrom(path))
imported.Add(Import(reader));
notification.Progress = (float)++i / paths.Length;
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with beatmaps from default storage.
// Also, not always a single file, i.e. for LegacyFilesystemReader
// TODO: Add a check to prevent files from storage to be deleted.
try
{
if (File.Exists(path))
File.Delete(path);
}
catch (Exception e)
{
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})");
}
}
catch (Exception e)
{
e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})");
}
}
notification.State = ProgressNotificationState.Completed;
return imported;
// remove metadata from difficulties where it matches the set
foreach (BeatmapInfo b in model.Beatmaps)
if (model.Metadata.Equals(b.Metadata))
b.Metadata = null;
}
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
/// </summary>
/// <param name="archive">The beatmap to be imported.</param>
public BeatmapSetInfo Import(ArchiveReader archive)
protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo beatmapSet)
{
using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
// check if this beatmap has already been imported and exit early if so
var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash);
if (existingHashMatch != null)
{
// create a new set info (don't yet add to database)
var beatmapSet = createBeatmapSetInfo(archive);
// check if this beatmap has already been imported and exit early if so
var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash);
if (existingHashMatch != null)
{
Undelete(existingHashMatch);
return existingHashMatch;
}
// check if a set already exists with the same online id
if (beatmapSet.OnlineBeatmapSetID != null)
{
var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.Cleanup(s => s.ID == existingOnlineId.ID);
}
}
beatmapSet.Files = createFileInfos(archive, files);
beatmapSet.Beatmaps = createBeatmapDifficulties(archive);
// remove metadata from difficulties where it matches the set
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
if (beatmapSet.Metadata.Equals(b.Metadata))
b.Metadata = null;
// import to beatmap store
Import(beatmapSet);
return beatmapSet;
Undelete(existingHashMatch);
return existingHashMatch;
}
// check if a set already exists with the same online id
if (beatmapSet.OnlineBeatmapSetID != null)
{
var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.Cleanup(s => s.ID == existingOnlineId.ID);
}
}
return null;
}
/// <summary>
@ -313,7 +224,7 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapSet">The beatmap set to delete.</param>
public void Delete(BeatmapSetInfo beatmapSet)
{
using (var usage = contextFactory.GetForWrite())
using (var usage = ContextFactory.GetForWrite())
{
var context = usage.Context;
@ -325,7 +236,7 @@ namespace osu.Game.Beatmaps
if (beatmaps.Delete(beatmapSet))
{
if (!beatmapSet.Protected)
files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
Files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
}
context.ChangeTracker.AutoDetectChangesEnabled = true;
@ -376,14 +287,14 @@ namespace osu.Game.Beatmaps
if (beatmapSet.Protected)
return;
using (var usage = contextFactory.GetForWrite())
using (var usage = ContextFactory.GetForWrite())
{
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
if (!beatmaps.Undelete(beatmapSet)) return;
if (!beatmapSet.Protected)
files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
Files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
}
@ -415,7 +326,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.Metadata == null)
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo);
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, beatmapInfo);
previous?.TransferTo(working);
@ -519,19 +430,6 @@ namespace osu.Game.Beatmaps
notification.State = ProgressNotificationState.Completed;
}
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the beatmap content.</param>
/// <returns>A reader giving access to the beatmap's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipFile.IsZipFile(path))
// ReSharper disable once InconsistentlySynchronizedField
return new OszArchiveReader(files.Storage.GetStream(path));
return new LegacyFilesystemReader(path);
}
/// <summary>
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
/// </summary>
@ -546,10 +444,7 @@ namespace osu.Game.Beatmaps
return hashable.ComputeSHA2Hash();
}
/// <summary>
/// Create a <see cref="BeatmapSetInfo"/> from a provided archive.
/// </summary>
private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader)
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
@ -568,25 +463,6 @@ namespace osu.Game.Beatmaps
};
}
/// <summary>
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
/// </summary>
private List<BeatmapSetFileInfo> createFileInfos(ArchiveReader reader, FileStore files)
{
List<BeatmapSetFileInfo> fileInfos = new List<BeatmapSetFileInfo>();
// import files to manager
foreach (string file in reader.Filenames)
using (Stream s = reader.GetStream(file))
fileInfos.Add(new BeatmapSetFileInfo
{
Filename = file,
FileInfo = files.Add(s)
});
return fileInfos;
}
/// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary>