mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 10:23:02 +08:00
Merge branch 'master' into fix-intro-realm-usage
This commit is contained in:
commit
33d3a10708
@ -5,7 +5,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
@ -151,9 +150,6 @@ namespace osu.Game.Database
|
||||
{
|
||||
Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
|
||||
|
||||
if (DebugUtils.IsDebugBuild)
|
||||
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
||||
|
||||
using (var source = storage.GetStream(DATABASE_NAME))
|
||||
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
||||
source.CopyTo(destination);
|
||||
|
@ -1,55 +1,127 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
internal class EFToRealmMigrator
|
||||
internal class EFToRealmMigrator : CompositeDrawable
|
||||
{
|
||||
private readonly DatabaseContextFactory efContextFactory;
|
||||
private readonly RealmContextFactory realmContextFactory;
|
||||
private readonly OsuConfigManager config;
|
||||
private readonly Storage storage;
|
||||
public bool FinishedMigrating { get; private set; }
|
||||
|
||||
public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config, Storage storage)
|
||||
[Resolved]
|
||||
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmContextFactory realmContextFactory { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly OsuSpriteText currentOperationText;
|
||||
|
||||
public EFToRealmMigrator()
|
||||
{
|
||||
this.efContextFactory = efContextFactory;
|
||||
this.realmContextFactory = realmContextFactory;
|
||||
this.config = config;
|
||||
this.storage = storage;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Database migration in progress",
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "This could take a few minutes depending on the speed of your disk(s).",
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Please keep the window open until this completes!",
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
new LoadingSpinner(true)
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
},
|
||||
currentOperationText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public void Run()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
createBackup();
|
||||
base.LoadComplete();
|
||||
|
||||
using (var ef = efContextFactory.Get())
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
migrateSettings(ef);
|
||||
migrateSkins(ef);
|
||||
migrateBeatmaps(ef);
|
||||
migrateScores(ef);
|
||||
}
|
||||
using (var ef = efContextFactory.Get())
|
||||
{
|
||||
migrateSettings(ef);
|
||||
migrateSkins(ef);
|
||||
migrateBeatmaps(ef);
|
||||
migrateScores(ef);
|
||||
}
|
||||
|
||||
// Delete the database permanently.
|
||||
// Will cause future startups to not attempt migration.
|
||||
Logger.Log("Migration successful, deleting EF database", LoggingTarget.Database);
|
||||
efContextFactory.ResetDatabase();
|
||||
// Delete the database permanently.
|
||||
// Will cause future startups to not attempt migration.
|
||||
log("Migration successful, deleting EF database");
|
||||
efContextFactory.ResetDatabase();
|
||||
|
||||
if (DebugUtils.IsDebugBuild)
|
||||
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||
{
|
||||
FinishedMigrating = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void log(string message)
|
||||
{
|
||||
Logger.Log(message, LoggingTarget.Database);
|
||||
Scheduler.AddOnce(m => currentOperationText.Text = m, message);
|
||||
}
|
||||
|
||||
private void migrateBeatmaps(OsuDbContext ef)
|
||||
@ -62,12 +134,12 @@ namespace osu.Game.Database
|
||||
.Include(s => s.Files).ThenInclude(f => f.FileInfo)
|
||||
.Include(s => s.Metadata);
|
||||
|
||||
Logger.Log("Beginning beatmaps migration to realm", LoggingTarget.Database);
|
||||
log("Beginning beatmaps migration to realm");
|
||||
|
||||
// previous entries in EF are removed post migration.
|
||||
if (!existingBeatmapSets.Any())
|
||||
{
|
||||
Logger.Log("No beatmaps found to migrate", LoggingTarget.Database);
|
||||
log("No beatmaps found to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -75,13 +147,13 @@ namespace osu.Game.Database
|
||||
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database);
|
||||
log($"Found {count} beatmaps in EF");
|
||||
|
||||
// only migrate data if the realm database is empty.
|
||||
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||
if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
|
||||
{
|
||||
Logger.Log("Skipping migration as realm already has beatmaps loaded", LoggingTarget.Database);
|
||||
log("Skipping migration as realm already has beatmaps loaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -96,7 +168,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
transaction.Commit();
|
||||
transaction = realm.BeginWrite();
|
||||
Logger.Log($"Migrated {written}/{count} beatmaps...", LoggingTarget.Database);
|
||||
log($"Migrated {written}/{count} beatmaps...");
|
||||
}
|
||||
|
||||
var realmBeatmapSet = new BeatmapSetInfo
|
||||
@ -156,7 +228,7 @@ namespace osu.Game.Database
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database);
|
||||
log($"Successfully migrated {count} beatmaps to realm");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -193,12 +265,12 @@ namespace osu.Game.Database
|
||||
.Include(s => s.Files)
|
||||
.ThenInclude(f => f.FileInfo);
|
||||
|
||||
Logger.Log("Beginning scores migration to realm", LoggingTarget.Database);
|
||||
log("Beginning scores migration to realm");
|
||||
|
||||
// previous entries in EF are removed post migration.
|
||||
if (!existingScores.Any())
|
||||
{
|
||||
Logger.Log("No scores found to migrate", LoggingTarget.Database);
|
||||
log("No scores found to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -206,12 +278,12 @@ namespace osu.Game.Database
|
||||
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
Logger.Log($"Found {count} scores in EF", LoggingTarget.Database);
|
||||
log($"Found {count} scores in EF");
|
||||
|
||||
// only migrate data if the realm database is empty.
|
||||
if (realm.All<ScoreInfo>().Any())
|
||||
{
|
||||
Logger.Log("Skipping migration as realm already has scores loaded", LoggingTarget.Database);
|
||||
log("Skipping migration as realm already has scores loaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -226,7 +298,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
transaction.Commit();
|
||||
transaction = realm.BeginWrite();
|
||||
Logger.Log($"Migrated {written}/{count} scores...", LoggingTarget.Database);
|
||||
log($"Migrated {written}/{count} scores...");
|
||||
}
|
||||
|
||||
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
|
||||
@ -270,7 +342,7 @@ namespace osu.Game.Database
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database);
|
||||
log($"Successfully migrated {count} scores to realm");
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -309,7 +381,7 @@ namespace osu.Game.Database
|
||||
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
||||
{
|
||||
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
|
||||
log($"Migrating {existingSkins.Count} skins");
|
||||
|
||||
foreach (var skin in existingSkins)
|
||||
{
|
||||
@ -358,7 +430,7 @@ namespace osu.Game.Database
|
||||
if (!existingSettings.Any())
|
||||
return;
|
||||
|
||||
Logger.Log("Beginning settings migration to realm", LoggingTarget.Database);
|
||||
log("Beginning settings migration to realm");
|
||||
|
||||
realmContextFactory.Run(realm =>
|
||||
{
|
||||
@ -367,7 +439,7 @@ namespace osu.Game.Database
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!realm.All<RealmRulesetSetting>().Any())
|
||||
{
|
||||
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
|
||||
log($"Migrating {existingSettings.Count} settings");
|
||||
|
||||
foreach (var dkb in existingSettings)
|
||||
{
|
||||
@ -396,17 +468,5 @@ namespace osu.Game.Database
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
||||
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
||||
|
||||
private void createBackup()
|
||||
{
|
||||
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
||||
|
||||
efContextFactory.CreateBackup($"client.{migration}.db");
|
||||
realmContextFactory.CreateBackup($"client.{migration}.realm");
|
||||
|
||||
using (var source = storage.GetStream("collection.db"))
|
||||
using (var destination = storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
|
||||
source.CopyTo(destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,6 +161,11 @@ namespace osu.Game
|
||||
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
|
||||
|
||||
/// <summary>
|
||||
/// A legacy EF context factory if migration has not been performed to realm yet.
|
||||
/// </summary>
|
||||
protected DatabaseContextFactory EFContextFactory { get; private set; }
|
||||
|
||||
public OsuGameBase()
|
||||
{
|
||||
UseDevelopmentServer = DebugUtils.IsDebugBuild;
|
||||
@ -184,18 +189,28 @@ namespace osu.Game
|
||||
|
||||
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
|
||||
|
||||
DatabaseContextFactory efContextFactory = Storage.Exists(DatabaseContextFactory.DATABASE_NAME)
|
||||
? new DatabaseContextFactory(Storage)
|
||||
: null;
|
||||
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
|
||||
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory));
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", EFContextFactory));
|
||||
|
||||
dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage));
|
||||
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
||||
|
||||
// A non-null context factory means there's still content to migrate.
|
||||
if (efContextFactory != null)
|
||||
new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig, Storage).Run();
|
||||
// Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts
|
||||
// after initial usages below. It can be moved once a direction is established for handling re-subscription.
|
||||
// See https://github.com/ppy/osu/pull/16547 for more discussion.
|
||||
if (EFContextFactory != null)
|
||||
{
|
||||
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
||||
|
||||
EFContextFactory.CreateBackup($"client.{migration}.db");
|
||||
realmFactory.CreateBackup($"client.{migration}.realm");
|
||||
|
||||
using (var source = Storage.GetStream("collection.db"))
|
||||
using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
|
||||
source.CopyTo(destination);
|
||||
}
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Screens.Menu;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using IntroSequence = osu.Game.Configuration.IntroSequence;
|
||||
|
||||
@ -63,6 +64,11 @@ namespace osu.Game.Screens
|
||||
|
||||
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DatabaseContextFactory efContextFactory { get; set; }
|
||||
|
||||
private EFToRealmMigrator realmMigrator;
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
@ -70,6 +76,10 @@ namespace osu.Game.Screens
|
||||
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||
|
||||
// A non-null context factory means there's still content to migrate.
|
||||
if (efContextFactory != null)
|
||||
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
|
||||
|
||||
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
@ -86,7 +96,7 @@ namespace osu.Game.Screens
|
||||
|
||||
private void checkIfLoaded()
|
||||
{
|
||||
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling)
|
||||
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false)
|
||||
{
|
||||
Schedule(checkIfLoaded);
|
||||
return;
|
||||
|
Loading…
Reference in New Issue
Block a user