2020-03-05 20:09:33 +03:00
using System ;
2021-11-24 13:00:12 +01:00
using System.Collections.Generic ;
using System.IO ;
2020-03-05 20:09:33 +03:00
using System.Linq ;
2025-03-27 03:23:36 +01:00
using System.Threading ;
using System.Threading.Tasks ;
2021-11-24 13:00:12 +01:00
using Emby.Server.Implementations ;
using Emby.Server.Implementations.Serialization ;
2025-03-27 03:23:36 +01:00
using Jellyfin.Database.Implementations ;
using Jellyfin.Server.Implementations ;
2020-03-05 20:09:33 +03:00
using MediaBrowser.Common.Configuration ;
2021-11-24 13:00:12 +01:00
using MediaBrowser.Model.Configuration ;
2025-03-27 03:23:36 +01:00
using Microsoft.EntityFrameworkCore.Storage ;
2020-04-16 23:40:32 -04:00
using Microsoft.Extensions.DependencyInjection ;
2020-03-05 18:21:27 +03:00
using Microsoft.Extensions.Logging ;
namespace Jellyfin.Server.Migrations
{
/// <summary>
2020-03-07 20:18:45 +01:00
/// The class that knows which migrations to apply and how to apply them.
2020-03-05 18:21:27 +03:00
/// </summary>
2020-03-05 20:09:33 +03:00
public sealed class MigrationRunner
2020-03-05 18:21:27 +03:00
{
2021-11-24 13:00:12 +01:00
/// <summary>
/// The list of known pre-startup migrations, in order of applicability.
/// </summary>
private static readonly Type [ ] _preStartupMigrationTypes =
{
2023-02-19 09:30:27 +01:00
typeof ( PreStartupRoutines . CreateNetworkConfiguration ) ,
2023-07-03 14:03:33 +02:00
typeof ( PreStartupRoutines . MigrateMusicBrainzTimeout ) ,
2024-09-09 16:43:37 +02:00
typeof ( PreStartupRoutines . MigrateNetworkConfiguration ) ,
typeof ( PreStartupRoutines . MigrateEncodingOptions )
2021-11-24 13:00:12 +01:00
} ;
2020-03-05 20:09:33 +03:00
/// <summary>
/// The list of known migrations, in order of applicability.
/// </summary>
2020-04-16 23:40:32 -04:00
private static readonly Type [ ] _migrationTypes =
2020-03-05 18:21:27 +03:00
{
2020-04-16 23:40:32 -04:00
typeof ( Routines . DisableTranscodingThrottling ) ,
2020-05-15 02:30:28 +03:00
typeof ( Routines . CreateUserLoggingConfigFile ) ,
2020-05-15 15:23:44 -04:00
typeof ( Routines . MigrateActivityLogDb ) ,
2020-06-05 13:23:38 -06:00
typeof ( Routines . RemoveDuplicateExtras ) ,
2020-06-17 02:16:17 +09:00
typeof ( Routines . AddDefaultPluginRepository ) ,
2020-06-30 21:44:41 -04:00
typeof ( Routines . MigrateUserDb ) ,
2020-07-23 23:50:12 +00:00
typeof ( Routines . ReaddDefaultPluginRepository ) ,
2020-11-10 00:20:12 +01:00
typeof ( Routines . MigrateDisplayPreferencesDb ) ,
2020-12-04 08:00:55 -07:00
typeof ( Routines . RemoveDownloadImagesInAdvance ) ,
2023-03-10 19:16:57 +01:00
typeof ( Routines . MigrateAuthenticationDb ) ,
2023-05-11 01:38:54 +02:00
typeof ( Routines . FixPlaylistOwner ) ,
2023-09-23 10:59:13 -06:00
typeof ( Routines . MigrateRatingLevels ) ,
2024-03-03 13:32:21 -07:00
typeof ( Routines . AddDefaultCastReceivers ) ,
2024-06-01 18:40:57 -04:00
typeof ( Routines . UpdateDefaultPluginRepository ) ,
typeof ( Routines . FixAudioData ) ,
2024-10-11 11:11:15 +00:00
typeof ( Routines . MoveTrickplayFiles ) ,
2024-11-19 20:53:38 +00:00
typeof ( Routines . RemoveDuplicatePlaylistChildren ) ,
2024-10-11 11:11:15 +00:00
typeof ( Routines . MigrateLibraryDb ) ,
2020-03-05 18:21:27 +03:00
} ;
/// <summary>
/// Run all needed migrations.
/// </summary>
/// <param name="host">CoreAppHost that hosts current version.</param>
2020-03-05 20:09:33 +03:00
/// <param name="loggerFactory">Factory for making the logger.</param>
2025-03-27 03:23:36 +01:00
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task Run ( CoreAppHost host , ILoggerFactory loggerFactory )
2020-03-05 18:21:27 +03:00
{
2020-03-05 20:09:33 +03:00
var logger = loggerFactory . CreateLogger < MigrationRunner > ( ) ;
2020-04-16 23:40:32 -04:00
var migrations = _migrationTypes
. Select ( m = > ActivatorUtilities . CreateInstance ( host . ServiceProvider , m ) )
. OfType < IMigrationRoutine > ( )
. ToArray ( ) ;
2021-11-24 13:00:12 +01:00
2021-08-04 14:40:09 +02:00
var migrationOptions = host . ConfigurationManager . GetConfiguration < MigrationOptions > ( MigrationsListStore . StoreKey ) ;
2021-11-24 13:00:12 +01:00
HandleStartupWizardCondition ( migrations , migrationOptions , host . ConfigurationManager . Configuration . IsStartupWizardCompleted , logger ) ;
2025-03-27 03:23:36 +01:00
await PerformMigrations ( migrations , migrationOptions , options = > host . ConfigurationManager . SaveConfiguration ( MigrationsListStore . StoreKey , options ) , logger , host . ServiceProvider . GetRequiredService < IJellyfinDatabaseProvider > ( ) )
. ConfigureAwait ( false ) ;
2021-11-24 13:00:12 +01:00
}
2020-03-06 13:22:44 +03:00
2021-11-24 13:00:12 +01:00
/// <summary>
/// Run all needed pre-startup migrations.
/// </summary>
/// <param name="appPaths">Application paths.</param>
/// <param name="loggerFactory">Factory for making the logger.</param>
2025-03-27 03:23:36 +01:00
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task RunPreStartup ( ServerApplicationPaths appPaths , ILoggerFactory loggerFactory )
2021-11-24 13:00:12 +01:00
{
var logger = loggerFactory . CreateLogger < MigrationRunner > ( ) ;
var migrations = _preStartupMigrationTypes
. Select ( m = > Activator . CreateInstance ( m , appPaths , loggerFactory ) )
. OfType < IMigrationRoutine > ( )
. ToArray ( ) ;
var xmlSerializer = new MyXmlSerializer ( ) ;
var migrationConfigPath = Path . Join ( appPaths . ConfigurationDirectoryPath , MigrationsListStore . StoreKey . ToLowerInvariant ( ) + ".xml" ) ;
2021-12-14 09:57:20 +02:00
var migrationOptions = File . Exists ( migrationConfigPath )
? ( MigrationOptions ) xmlSerializer . DeserializeFromFile ( typeof ( MigrationOptions ) , migrationConfigPath ) !
: new MigrationOptions ( ) ;
2021-11-24 13:00:12 +01:00
// We have to deserialize it manually since the configuration manager may overwrite it
2021-12-14 19:27:23 +01:00
var serverConfig = File . Exists ( appPaths . SystemConfigurationFilePath )
? ( ServerConfiguration ) xmlSerializer . DeserializeFromFile ( typeof ( ServerConfiguration ) , appPaths . SystemConfigurationFilePath ) !
: new ServerConfiguration ( ) ;
2021-11-24 13:00:12 +01:00
HandleStartupWizardCondition ( migrations , migrationOptions , serverConfig . IsStartupWizardCompleted , logger ) ;
2025-03-27 03:23:36 +01:00
await PerformMigrations ( migrations , migrationOptions , options = > xmlSerializer . SerializeToFile ( options , migrationConfigPath ) , logger , null ) . ConfigureAwait ( false ) ;
2021-11-24 13:00:12 +01:00
}
private static void HandleStartupWizardCondition ( IEnumerable < IMigrationRoutine > migrations , MigrationOptions migrationOptions , bool isStartWizardCompleted , ILogger logger )
{
2023-07-11 02:13:55 +02:00
if ( isStartWizardCompleted )
2020-03-06 13:22:44 +03:00
{
2021-11-24 13:00:12 +01:00
return ;
2020-03-06 13:22:44 +03:00
}
2021-11-24 13:00:12 +01:00
// If startup wizard is not finished, this is a fresh install.
var onlyOldInstalls = migrations . Where ( m = > ! m . PerformOnNewInstall ) . ToArray ( ) ;
logger . LogInformation ( "Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}" , onlyOldInstalls . Select ( m = > m . Name ) ) ;
migrationOptions . Applied . AddRange ( onlyOldInstalls . Select ( m = > ( m . Id , m . Name ) ) ) ;
}
2025-03-27 03:23:36 +01:00
private static async Task PerformMigrations (
IMigrationRoutine [ ] migrations ,
MigrationOptions migrationOptions ,
Action < MigrationOptions > saveConfiguration ,
ILogger logger ,
IJellyfinDatabaseProvider ? jellyfinDatabaseProvider )
2021-11-24 13:00:12 +01:00
{
2023-07-11 02:13:55 +02:00
// save already applied migrations, and skip them thereafter
saveConfiguration ( migrationOptions ) ;
2020-03-08 17:40:30 +01:00
var appliedMigrationIds = migrationOptions . Applied . Select ( m = > m . Id ) . ToHashSet ( ) ;
2025-03-27 03:23:36 +01:00
var migrationsToBeApplied = migrations . Where ( e = > ! appliedMigrationIds . Contains ( e . Id ) ) . ToArray ( ) ;
2020-03-08 17:40:30 +01:00
2025-03-27 03:23:36 +01:00
string? migrationKey = null ;
if ( jellyfinDatabaseProvider is not null & & migrationsToBeApplied . Any ( f = > f is IDatabaseMigrationRoutine ) )
2020-03-05 18:21:27 +03:00
{
2025-03-27 03:23:36 +01:00
logger . LogInformation ( "Performing database backup" ) ;
2020-03-05 20:09:33 +03:00
try
2020-03-05 18:21:27 +03:00
{
2025-03-27 03:23:36 +01:00
migrationKey = await jellyfinDatabaseProvider . MigrationBackupFast ( CancellationToken . None ) . ConfigureAwait ( false ) ;
logger . LogInformation ( "Database backup with key '{BackupKey}' has been successfully created." , migrationKey ) ;
2020-03-05 20:09:33 +03:00
}
2025-03-27 03:23:36 +01:00
catch ( NotImplementedException )
2020-03-05 20:09:33 +03:00
{
2025-03-27 03:23:36 +01:00
logger . LogWarning ( "Could not perform backup of database before migration because provider does not support it" ) ;
2020-03-05 18:21:27 +03:00
}
2025-03-27 03:23:36 +01:00
}
try
{
foreach ( var migrationRoutine in migrationsToBeApplied )
{
logger . LogInformation ( "Applying migration '{Name}'" , migrationRoutine . Name ) ;
try
{
migrationRoutine . Perform ( ) ;
}
catch ( Exception ex )
{
logger . LogError ( ex , "Could not apply migration '{Name}'" , migrationRoutine . Name ) ;
throw ;
}
2020-03-05 18:21:27 +03:00
2025-03-27 03:23:36 +01:00
// Mark the migration as completed
logger . LogInformation ( "Migration '{Name}' applied successfully" , migrationRoutine . Name ) ;
migrationOptions . Applied . Add ( ( migrationRoutine . Id , migrationRoutine . Name ) ) ;
saveConfiguration ( migrationOptions ) ;
logger . LogDebug ( "Migration '{Name}' marked as applied in configuration." , migrationRoutine . Name ) ;
}
}
catch ( System . Exception ) when ( migrationKey is not null & & jellyfinDatabaseProvider is not null )
{
logger . LogInformation ( "Rollback on database as migration reported failure." ) ;
await jellyfinDatabaseProvider . RestoreBackupFast ( migrationKey , CancellationToken . None ) . ConfigureAwait ( false ) ;
throw ;
2020-03-05 20:09:33 +03:00
}
2020-03-05 18:21:27 +03:00
}
}
}