mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 13:47:38 +08:00
Merge pull request #4453 from peppy/ensure-import-sanity
Ensure import continues if an existing item's online IDs are in a bad state
This commit is contained in:
commit
47e28e8ad6
@ -207,6 +207,41 @@ namespace osu.Game.Tests.Beatmaps.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
|
||||||
|
{
|
||||||
|
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
|
||||||
|
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var osu = loadOsu(host);
|
||||||
|
|
||||||
|
var imported = LoadOszIntoOsu(osu);
|
||||||
|
|
||||||
|
if (set)
|
||||||
|
imported.OnlineBeatmapSetID = 1234;
|
||||||
|
else
|
||||||
|
imported.Beatmaps.First().OnlineBeatmapID = 1234;
|
||||||
|
|
||||||
|
osu.Dependencies.Get<BeatmapManager>().Update(imported);
|
||||||
|
|
||||||
|
deleteBeatmapSet(imported, osu);
|
||||||
|
|
||||||
|
var importedSecondTime = LoadOszIntoOsu(osu);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
|
||||||
|
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
host.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[NonParallelizable]
|
[NonParallelizable]
|
||||||
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
|
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
|
||||||
|
@ -102,11 +102,14 @@ namespace osu.Game.Beatmaps
|
|||||||
b.BeatmapSet = beatmapSet;
|
b.BeatmapSet = beatmapSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOnlineIds(beatmapSet.Beatmaps);
|
validateOnlineIds(beatmapSet);
|
||||||
|
|
||||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||||
fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
|
fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PreImport(BeatmapSetInfo beatmapSet)
|
||||||
|
{
|
||||||
// check if a set already exists with the same online id, delete if it does.
|
// check if a set already exists with the same online id, delete if it does.
|
||||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||||
{
|
{
|
||||||
@ -120,14 +123,30 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateOnlineIds(List<BeatmapInfo> beatmaps)
|
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
|
||||||
{
|
{
|
||||||
var beatmapIds = beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
||||||
|
|
||||||
// ensure all IDs are unique in this set and none match existing IDs in the local beatmap store.
|
// ensure all IDs are unique
|
||||||
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any())
|
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||||
// remove all online IDs if any problems were found.
|
{
|
||||||
beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
resetIds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find any existing beatmaps in the database that have matching online ids
|
||||||
|
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
|
||||||
|
|
||||||
|
if (existingBeatmaps.Count > 0)
|
||||||
|
{
|
||||||
|
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
|
||||||
|
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
|
||||||
|
var existing = CheckForExisting(beatmapSet);
|
||||||
|
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
|
||||||
|
resetIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -254,6 +273,18 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||||
|
|
||||||
|
protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||||
|
{
|
||||||
|
if (!base.CanUndelete(existing, import))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||||
|
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||||
|
|
||||||
|
// force re-import if we are not in a sane state.
|
||||||
|
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -300,20 +300,30 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
|
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
|
||||||
|
|
||||||
|
if (archive != null)
|
||||||
|
item.Files = createFileInfos(archive, Files);
|
||||||
|
|
||||||
|
Populate(item, archive);
|
||||||
|
|
||||||
var existing = CheckForExisting(item);
|
var existing = CheckForExisting(item);
|
||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
|
{
|
||||||
|
if (CanUndelete(existing, item))
|
||||||
{
|
{
|
||||||
Undelete(existing);
|
Undelete(existing);
|
||||||
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
|
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
|
||||||
handleEvent(() => ItemAdded?.Invoke(existing, true));
|
handleEvent(() => ItemAdded?.Invoke(existing, true));
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Delete(existing);
|
||||||
|
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (archive != null)
|
PreImport(item);
|
||||||
item.Files = createFileInfos(archive, Files);
|
|
||||||
|
|
||||||
Populate(item, archive);
|
|
||||||
|
|
||||||
// import to store
|
// import to store
|
||||||
ModelStore.Add(item);
|
ModelStore.Add(item);
|
||||||
@ -542,12 +552,29 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform any final actions before the import to database executes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The model prepared for import.</param>
|
||||||
|
protected virtual void PreImport(TModel model)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check whether an existing model already exists for a new import item.
|
/// Check whether an existing model already exists for a new import item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">The new model proposed for import. Note that <see cref="Populate"/> has not yet been run on this model.</param>
|
/// <param name="model">The new model proposed for import.
|
||||||
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
||||||
protected virtual TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
|
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// After an existing <see cref="TModel"/> is found during an import process, the default behaviour is to restore the existing
|
||||||
|
/// item and skip the import. This method allows changing that behaviour.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="existing">The existing model.</param>
|
||||||
|
/// <param name="import">The newly imported model.</param>
|
||||||
|
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import.</returns>
|
||||||
|
protected virtual bool CanUndelete(TModel existing, TModel import) => true;
|
||||||
|
|
||||||
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user