2021-11-30 06:41:18 +00:00
// 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.Linq ;
2022-01-21 05:56:49 +00:00
using System.Threading.Tasks ;
2021-11-30 06:41:18 +00:00
using Microsoft.EntityFrameworkCore ;
2022-01-21 05:56:49 +00:00
using osu.Framework.Allocation ;
2022-01-22 13:20:28 +00:00
using osu.Framework.Development ;
2022-01-21 05:56:49 +00:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2022-01-18 05:19:31 +00:00
using osu.Framework.Logging ;
2022-01-13 08:51:23 +00:00
using osu.Game.Beatmaps ;
2021-11-30 06:41:18 +00:00
using osu.Game.Configuration ;
2022-01-21 05:56:49 +00:00
using osu.Game.Graphics ;
using osu.Game.Graphics.Sprites ;
using osu.Game.Graphics.UserInterface ;
2021-11-30 06:41:18 +00:00
using osu.Game.Models ;
2022-01-13 08:51:23 +00:00
using osu.Game.Rulesets ;
using osu.Game.Scoring ;
2021-11-30 06:41:18 +00:00
using osu.Game.Skinning ;
2022-01-21 05:56:49 +00:00
using osuTK ;
2022-01-13 08:51:23 +00:00
using Realms ;
2021-11-30 06:41:18 +00:00
#nullable enable
namespace osu.Game.Database
{
2022-01-21 05:56:49 +00:00
internal class EFToRealmMigrator : CompositeDrawable
2021-11-30 06:41:18 +00:00
{
2022-01-21 05:56:49 +00:00
public bool FinishedMigrating { get ; private set ; }
2021-11-30 06:41:18 +00:00
2022-01-21 05:56:49 +00:00
[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 ( )
2021-11-30 06:41:18 +00:00
{
2022-01-21 05:56:49 +00:00
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 )
} ,
}
} ,
} ;
2021-11-30 06:41:18 +00:00
}
2022-01-21 05:56:49 +00:00
protected override void LoadComplete ( )
2021-11-30 06:41:18 +00:00
{
2022-01-21 05:56:49 +00:00
base . LoadComplete ( ) ;
Task . Factory . StartNew ( ( ) = >
2022-01-18 11:47:53 +00:00
{
2022-01-21 05:56:49 +00:00
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.
log ( "Migration successful, deleting EF database" ) ;
efContextFactory . ResetDatabase ( ) ;
2022-01-22 13:20:28 +00:00
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 ) ;
2022-01-21 05:56:49 +00:00
} , TaskCreationOptions . LongRunning ) . ContinueWith ( t = >
{
FinishedMigrating = true ;
} ) ;
}
2022-01-18 05:17:43 +00:00
2022-01-21 05:56:49 +00:00
private void log ( string message )
{
Logger . Log ( message , LoggingTarget . Database ) ;
Scheduler . AddOnce ( m = > currentOperationText . Text = m , message ) ;
2022-01-13 08:51:23 +00:00
}
2022-01-19 06:52:59 +00:00
private void migrateBeatmaps ( OsuDbContext ef )
2022-01-13 09:02:08 +00:00
{
// can be removed 20220730.
2022-01-19 06:52:59 +00:00
var existingBeatmapSets = ef . EFBeatmapSetInfo
2022-01-18 10:12:10 +00:00
. Include ( s = > s . Beatmaps ) . ThenInclude ( b = > b . RulesetInfo )
. Include ( s = > s . Beatmaps ) . ThenInclude ( b = > b . Metadata )
. Include ( s = > s . Beatmaps ) . ThenInclude ( b = > b . BaseDifficulty )
. Include ( s = > s . Files ) . ThenInclude ( f = > f . FileInfo )
. Include ( s = > s . Metadata ) ;
2022-01-13 09:02:08 +00:00
2022-01-21 05:56:49 +00:00
log ( "Beginning beatmaps migration to realm" ) ;
2022-01-18 05:41:02 +00:00
2022-01-13 09:02:08 +00:00
// previous entries in EF are removed post migration.
if ( ! existingBeatmapSets . Any ( ) )
2022-01-18 05:41:02 +00:00
{
2022-01-21 05:56:49 +00:00
log ( "No beatmaps found to migrate" ) ;
2022-01-13 09:02:08 +00:00
return ;
2022-01-18 05:41:02 +00:00
}
2022-01-18 05:19:31 +00:00
2022-01-18 10:12:10 +00:00
int count = existingBeatmapSets . Count ( ) ;
2022-01-21 08:08:20 +00:00
realmContextFactory . Run ( realm = >
2022-01-13 09:02:08 +00:00
{
2022-01-21 05:56:49 +00:00
log ( $"Found {count} beatmaps in EF" ) ;
2022-01-19 01:16:54 +00:00
2022-01-13 09:02:08 +00:00
// only migrate data if the realm database is empty.
2022-01-19 01:20:43 +00:00
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
2022-01-18 05:41:02 +00:00
if ( realm . All < BeatmapSetInfo > ( ) . Any ( s = > ! s . Protected ) )
{
2022-01-21 05:56:49 +00:00
log ( "Skipping migration as realm already has beatmaps loaded" ) ;
2022-01-18 05:41:02 +00:00
}
else
2022-01-13 09:02:08 +00:00
{
2022-01-19 07:01:17 +00:00
var transaction = realm . BeginWrite ( ) ;
int written = 0 ;
try
2022-01-13 09:02:08 +00:00
{
2022-01-18 05:41:02 +00:00
foreach ( var beatmapSet in existingBeatmapSets )
2022-01-13 09:02:08 +00:00
{
2022-01-19 07:01:17 +00:00
if ( + + written % 1000 = = 0 )
{
transaction . Commit ( ) ;
transaction = realm . BeginWrite ( ) ;
2022-01-21 05:56:49 +00:00
log ( $"Migrated {written}/{count} beatmaps..." ) ;
2022-01-19 07:01:17 +00:00
}
2022-01-18 05:41:02 +00:00
var realmBeatmapSet = new BeatmapSetInfo
2022-01-13 09:02:08 +00:00
{
2022-01-18 05:41:02 +00:00
OnlineID = beatmapSet . OnlineID ? ? - 1 ,
DateAdded = beatmapSet . DateAdded ,
Status = beatmapSet . Status ,
DeletePending = beatmapSet . DeletePending ,
Hash = beatmapSet . Hash ,
Protected = beatmapSet . Protected ,
2022-01-13 09:02:08 +00:00
} ;
2022-01-18 05:41:02 +00:00
migrateFiles ( beatmapSet , realm , realmBeatmapSet ) ;
foreach ( var beatmap in beatmapSet . Beatmaps )
{
2022-01-19 07:22:17 +00:00
var ruleset = realm . Find < RulesetInfo > ( beatmap . RulesetInfo . ShortName ) ;
var metadata = getBestMetadata ( beatmap . Metadata , beatmapSet . Metadata ) ;
var realmBeatmap = new BeatmapInfo ( ruleset , new BeatmapDifficulty ( beatmap . BaseDifficulty ) , metadata )
2022-01-18 05:41:02 +00:00
{
DifficultyName = beatmap . DifficultyName ,
Status = beatmap . Status ,
OnlineID = beatmap . OnlineID ? ? - 1 ,
Length = beatmap . Length ,
BPM = beatmap . BPM ,
Hash = beatmap . Hash ,
StarRating = beatmap . StarRating ,
MD5Hash = beatmap . MD5Hash ,
Hidden = beatmap . Hidden ,
AudioLeadIn = beatmap . AudioLeadIn ,
StackLeniency = beatmap . StackLeniency ,
SpecialStyle = beatmap . SpecialStyle ,
LetterboxInBreaks = beatmap . LetterboxInBreaks ,
WidescreenStoryboard = beatmap . WidescreenStoryboard ,
EpilepsyWarning = beatmap . EpilepsyWarning ,
SamplesMatchPlaybackRate = beatmap . SamplesMatchPlaybackRate ,
DistanceSpacing = beatmap . DistanceSpacing ,
BeatDivisor = beatmap . BeatDivisor ,
GridSize = beatmap . GridSize ,
TimelineZoom = beatmap . TimelineZoom ,
Countdown = beatmap . Countdown ,
CountdownOffset = beatmap . CountdownOffset ,
MaxCombo = beatmap . MaxCombo ,
Bookmarks = beatmap . Bookmarks ,
BeatmapSet = realmBeatmapSet ,
} ;
realmBeatmapSet . Beatmaps . Add ( realmBeatmap ) ;
}
realm . Add ( realmBeatmapSet ) ;
2022-01-13 09:02:08 +00:00
}
2022-01-19 07:01:17 +00:00
}
finally
{
2022-01-18 05:41:02 +00:00
transaction . Commit ( ) ;
2022-01-13 09:02:08 +00:00
}
2022-01-19 07:01:17 +00:00
2022-01-21 05:56:49 +00:00
log ( $"Successfully migrated {count} beatmaps to realm" ) ;
2022-01-13 09:02:08 +00:00
}
2022-01-21 08:08:20 +00:00
} ) ;
2022-01-13 09:02:08 +00:00
}
2022-01-14 14:31:42 +00:00
private BeatmapMetadata getBestMetadata ( EFBeatmapMetadata ? beatmapMetadata , EFBeatmapMetadata ? beatmapSetMetadata )
{
var metadata = beatmapMetadata ? ? beatmapSetMetadata ? ? new EFBeatmapMetadata ( ) ;
return new BeatmapMetadata
{
Title = metadata . Title ,
TitleUnicode = metadata . TitleUnicode ,
Artist = metadata . Artist ,
ArtistUnicode = metadata . ArtistUnicode ,
2022-01-19 07:22:17 +00:00
Author =
2022-01-14 14:31:42 +00:00
{
OnlineID = metadata . Author . Id ,
Username = metadata . Author . Username ,
} ,
Source = metadata . Source ,
Tags = metadata . Tags ,
PreviewTime = metadata . PreviewTime ,
AudioFile = metadata . AudioFile ,
BackgroundFile = metadata . BackgroundFile ,
} ;
}
2022-01-19 06:52:59 +00:00
private void migrateScores ( OsuDbContext db )
2022-01-13 08:51:23 +00:00
{
// can be removed 20220730.
2022-01-19 06:52:59 +00:00
var existingScores = db . ScoreInfo
2022-01-18 10:12:10 +00:00
. Include ( s = > s . Ruleset )
. Include ( s = > s . BeatmapInfo )
. Include ( s = > s . Files )
. ThenInclude ( f = > f . FileInfo ) ;
2022-01-13 08:51:23 +00:00
2022-01-21 05:56:49 +00:00
log ( "Beginning scores migration to realm" ) ;
2022-01-18 05:41:02 +00:00
2022-01-13 08:51:23 +00:00
// previous entries in EF are removed post migration.
if ( ! existingScores . Any ( ) )
2022-01-18 05:41:02 +00:00
{
2022-01-21 05:56:49 +00:00
log ( "No scores found to migrate" ) ;
2022-01-13 08:51:23 +00:00
return ;
2022-01-18 05:41:02 +00:00
}
2022-01-18 05:19:31 +00:00
2022-01-18 10:12:10 +00:00
int count = existingScores . Count ( ) ;
2022-01-21 08:08:20 +00:00
realmContextFactory . Run ( realm = >
2022-01-13 08:51:23 +00:00
{
2022-01-21 05:56:49 +00:00
log ( $"Found {count} scores in EF" ) ;
2022-01-19 01:16:54 +00:00
2022-01-13 08:51:23 +00:00
// only migrate data if the realm database is empty.
2022-01-18 05:41:02 +00:00
if ( realm . All < ScoreInfo > ( ) . Any ( ) )
{
2022-01-21 05:56:49 +00:00
log ( "Skipping migration as realm already has scores loaded" ) ;
2022-01-18 05:41:02 +00:00
}
else
2022-01-13 08:51:23 +00:00
{
2022-01-19 07:01:17 +00:00
var transaction = realm . BeginWrite ( ) ;
int written = 0 ;
try
2022-01-13 08:51:23 +00:00
{
2022-01-18 05:41:02 +00:00
foreach ( var score in existingScores )
2022-01-13 08:51:23 +00:00
{
2022-01-19 07:01:17 +00:00
if ( + + written % 1000 = = 0 )
{
transaction . Commit ( ) ;
transaction = realm . BeginWrite ( ) ;
2022-01-21 05:56:49 +00:00
log ( $"Migrated {written}/{count} scores..." ) ;
2022-01-19 07:01:17 +00:00
}
2022-01-19 07:22:17 +00:00
var beatmap = realm . All < BeatmapInfo > ( ) . First ( b = > b . Hash = = score . BeatmapInfo . Hash ) ;
var ruleset = realm . Find < RulesetInfo > ( score . Ruleset . ShortName ) ;
var user = new RealmUser
{
OnlineID = score . User . OnlineID ,
Username = score . User . Username
} ;
var realmScore = new ScoreInfo ( beatmap , ruleset , user )
2022-01-18 05:41:02 +00:00
{
Hash = score . Hash ,
DeletePending = score . DeletePending ,
OnlineID = score . OnlineID ? ? - 1 ,
ModsJson = score . ModsJson ,
StatisticsJson = score . StatisticsJson ,
TotalScore = score . TotalScore ,
MaxCombo = score . MaxCombo ,
Accuracy = score . Accuracy ,
HasReplay = ( ( IScoreInfo ) score ) . HasReplay ,
Date = score . Date ,
PP = score . PP ,
Rank = score . Rank ,
HitEvents = score . HitEvents ,
Passed = score . Passed ,
Combo = score . Combo ,
Position = score . Position ,
Statistics = score . Statistics ,
Mods = score . Mods ,
APIMods = score . APIMods ,
} ;
migrateFiles ( score , realm , realmScore ) ;
realm . Add ( realmScore ) ;
}
2022-01-19 07:01:17 +00:00
}
finally
{
2022-01-18 05:41:02 +00:00
transaction . Commit ( ) ;
2022-01-13 08:51:23 +00:00
}
2022-01-19 07:01:17 +00:00
2022-01-21 05:56:49 +00:00
log ( $"Successfully migrated {count} scores to realm" ) ;
2022-01-13 08:51:23 +00:00
}
2022-01-21 08:08:20 +00:00
} ) ;
2021-11-30 06:41:18 +00:00
}
2022-01-19 06:52:59 +00:00
private void migrateSkins ( OsuDbContext db )
2021-11-30 06:41:18 +00:00
{
2021-12-06 19:12:02 +00:00
// can be removed 20220530.
2022-01-19 06:52:59 +00:00
var existingSkins = db . SkinInfo
2021-12-06 19:12:02 +00:00
. Include ( s = > s . Files )
. ThenInclude ( f = > f . FileInfo )
. ToList ( ) ;
// previous entries in EF are removed post migration.
if ( ! existingSkins . Any ( ) )
return ;
2021-11-30 06:41:18 +00:00
var userSkinChoice = config . GetBindable < string > ( OsuSetting . Skin ) ;
int . TryParse ( userSkinChoice . Value , out int userSkinInt ) ;
switch ( userSkinInt )
{
case EFSkinInfo . DEFAULT_SKIN :
userSkinChoice . Value = SkinInfo . DEFAULT_SKIN . ToString ( ) ;
break ;
case EFSkinInfo . CLASSIC_SKIN :
userSkinChoice . Value = SkinInfo . CLASSIC_SKIN . ToString ( ) ;
break ;
}
2022-01-21 08:08:20 +00:00
realmContextFactory . Run ( realm = >
2021-11-30 06:41:18 +00:00
{
2022-01-21 08:08:20 +00:00
using ( var transaction = realm . BeginWrite ( ) )
2021-11-30 06:41:18 +00:00
{
2022-01-21 08:08:20 +00:00
// only migrate data if the realm database is empty.
// 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 ) )
2021-11-30 06:41:18 +00:00
{
2022-01-22 12:52:19 +00:00
log ( $"Migrating {existingSkins.Count} skins" ) ;
2022-01-21 08:08:20 +00:00
foreach ( var skin in existingSkins )
2021-11-30 06:41:18 +00:00
{
2022-01-21 08:08:20 +00:00
var realmSkin = new SkinInfo
{
Name = skin . Name ,
Creator = skin . Creator ,
Hash = skin . Hash ,
Protected = false ,
InstantiationInfo = skin . InstantiationInfo ,
} ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
migrateFiles ( skin , realm , realmSkin ) ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
realm . Add ( realmSkin ) ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
if ( skin . ID = = userSkinInt )
userSkinChoice . Value = realmSkin . ID . ToString ( ) ;
}
2021-11-30 06:41:18 +00:00
}
2022-01-21 08:08:20 +00:00
transaction . Commit ( ) ;
}
} ) ;
2021-11-30 06:41:18 +00:00
}
2022-01-13 08:51:23 +00:00
private static void migrateFiles < T > ( IHasFiles < T > fileSource , Realm realm , IHasRealmFiles realmObject ) where T : INamedFileInfo
{
foreach ( var file in fileSource . Files )
{
var realmFile = realm . Find < RealmFile > ( file . FileInfo . Hash ) ;
if ( realmFile = = null )
realm . Add ( realmFile = new RealmFile { Hash = file . FileInfo . Hash } ) ;
realmObject . Files . Add ( new RealmNamedFileUsage ( realmFile , file . Filename ) ) ;
}
}
2022-01-19 06:52:59 +00:00
private void migrateSettings ( OsuDbContext db )
2021-11-30 06:41:18 +00:00
{
// migrate ruleset settings. can be removed 20220315.
2022-01-19 06:52:59 +00:00
var existingSettings = db . DatabasedSetting . ToList ( ) ;
2021-11-30 06:41:18 +00:00
// previous entries in EF are removed post migration.
if ( ! existingSettings . Any ( ) )
return ;
2022-01-21 05:56:49 +00:00
log ( "Beginning settings migration to realm" ) ;
2022-01-18 05:19:31 +00:00
2022-01-21 08:08:20 +00:00
realmContextFactory . Run ( realm = >
2021-11-30 06:41:18 +00:00
{
2022-01-21 08:08:20 +00:00
using ( var transaction = realm . BeginWrite ( ) )
2021-11-30 06:41:18 +00:00
{
2022-01-21 08:08:20 +00:00
// only migrate data if the realm database is empty.
if ( ! realm . All < RealmRulesetSetting > ( ) . Any ( ) )
2021-11-30 06:41:18 +00:00
{
2022-01-22 12:52:19 +00:00
log ( $"Migrating {existingSettings.Count} settings" ) ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
foreach ( var dkb in existingSettings )
{
if ( dkb . RulesetID = = null )
continue ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
string? shortName = getRulesetShortNameFromLegacyID ( dkb . RulesetID . Value ) ;
2021-11-30 06:41:18 +00:00
2022-01-21 08:08:20 +00:00
if ( string . IsNullOrEmpty ( shortName ) )
continue ;
realm . Add ( new RealmRulesetSetting
{
Key = dkb . Key ,
Value = dkb . StringValue ,
RulesetName = shortName ,
Variant = dkb . Variant ? ? 0 ,
} ) ;
}
2021-11-30 06:41:18 +00:00
}
2022-01-21 08:08:20 +00:00
transaction . Commit ( ) ;
}
} ) ;
2021-11-30 06:41:18 +00:00
}
private string? getRulesetShortNameFromLegacyID ( long rulesetId ) = >
efContextFactory . Get ( ) . RulesetInfo . FirstOrDefault ( r = > r . ID = = rulesetId ) ? . ShortName ;
}
}