| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.IO; |
| | 4 | | using System.Linq; |
| | 5 | | using System.Threading; |
| | 6 | | using System.Threading.Tasks; |
| | 7 | | using Emby.Server.Implementations; |
| | 8 | | using Emby.Server.Implementations.Serialization; |
| | 9 | | using Jellyfin.Database.Implementations; |
| | 10 | | using Jellyfin.Server.Implementations; |
| | 11 | | using MediaBrowser.Common.Configuration; |
| | 12 | | using MediaBrowser.Model.Configuration; |
| | 13 | | using Microsoft.EntityFrameworkCore.Storage; |
| | 14 | | using Microsoft.Extensions.DependencyInjection; |
| | 15 | | using Microsoft.Extensions.Logging; |
| | 16 | |
|
| | 17 | | namespace Jellyfin.Server.Migrations |
| | 18 | | { |
| | 19 | | /// <summary> |
| | 20 | | /// The class that knows which migrations to apply and how to apply them. |
| | 21 | | /// </summary> |
| | 22 | | public sealed class MigrationRunner |
| | 23 | | { |
| | 24 | | /// <summary> |
| | 25 | | /// The list of known pre-startup migrations, in order of applicability. |
| | 26 | | /// </summary> |
| 0 | 27 | | private static readonly Type[] _preStartupMigrationTypes = |
| 0 | 28 | | { |
| 0 | 29 | | typeof(PreStartupRoutines.CreateNetworkConfiguration), |
| 0 | 30 | | typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), |
| 0 | 31 | | typeof(PreStartupRoutines.MigrateNetworkConfiguration), |
| 0 | 32 | | typeof(PreStartupRoutines.MigrateEncodingOptions), |
| 0 | 33 | | typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections) |
| 0 | 34 | | }; |
| | 35 | |
|
| | 36 | | /// <summary> |
| | 37 | | /// The list of known migrations, in order of applicability. |
| | 38 | | /// </summary> |
| 0 | 39 | | private static readonly Type[] _migrationTypes = |
| 0 | 40 | | { |
| 0 | 41 | | typeof(Routines.DisableTranscodingThrottling), |
| 0 | 42 | | typeof(Routines.CreateUserLoggingConfigFile), |
| 0 | 43 | | typeof(Routines.MigrateActivityLogDb), |
| 0 | 44 | | typeof(Routines.RemoveDuplicateExtras), |
| 0 | 45 | | typeof(Routines.AddDefaultPluginRepository), |
| 0 | 46 | | typeof(Routines.MigrateUserDb), |
| 0 | 47 | | typeof(Routines.ReaddDefaultPluginRepository), |
| 0 | 48 | | typeof(Routines.MigrateDisplayPreferencesDb), |
| 0 | 49 | | typeof(Routines.RemoveDownloadImagesInAdvance), |
| 0 | 50 | | typeof(Routines.MigrateAuthenticationDb), |
| 0 | 51 | | typeof(Routines.FixPlaylistOwner), |
| 0 | 52 | | typeof(Routines.AddDefaultCastReceivers), |
| 0 | 53 | | typeof(Routines.UpdateDefaultPluginRepository), |
| 0 | 54 | | typeof(Routines.FixAudioData), |
| 0 | 55 | | typeof(Routines.RemoveDuplicatePlaylistChildren), |
| 0 | 56 | | typeof(Routines.MigrateLibraryDb), |
| 0 | 57 | | typeof(Routines.MoveExtractedFiles), |
| 0 | 58 | | typeof(Routines.MigrateRatingLevels), |
| 0 | 59 | | typeof(Routines.MoveTrickplayFiles), |
| 0 | 60 | | typeof(Routines.MigrateKeyframeData), |
| 0 | 61 | | }; |
| | 62 | |
|
| | 63 | | /// <summary> |
| | 64 | | /// Run all needed migrations. |
| | 65 | | /// </summary> |
| | 66 | | /// <param name="host">CoreAppHost that hosts current version.</param> |
| | 67 | | /// <param name="loggerFactory">Factory for making the logger.</param> |
| | 68 | | /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> |
| | 69 | | public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory) |
| | 70 | | { |
| | 71 | | var logger = loggerFactory.CreateLogger<MigrationRunner>(); |
| | 72 | | var migrations = _migrationTypes |
| | 73 | | .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) |
| | 74 | | .OfType<IMigrationRoutine>() |
| | 75 | | .ToArray(); |
| | 76 | |
|
| | 77 | | var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.Stor |
| | 78 | | HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartup |
| | 79 | | await PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration |
| | 80 | | .ConfigureAwait(false); |
| | 81 | | } |
| | 82 | |
|
| | 83 | | /// <summary> |
| | 84 | | /// Run all needed pre-startup migrations. |
| | 85 | | /// </summary> |
| | 86 | | /// <param name="appPaths">Application paths.</param> |
| | 87 | | /// <param name="loggerFactory">Factory for making the logger.</param> |
| | 88 | | /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> |
| | 89 | | public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory) |
| | 90 | | { |
| | 91 | | var logger = loggerFactory.CreateLogger<MigrationRunner>(); |
| | 92 | | var migrations = _preStartupMigrationTypes |
| | 93 | | .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory)) |
| | 94 | | .OfType<IMigrationRoutine>() |
| | 95 | | .ToArray(); |
| | 96 | |
|
| | 97 | | var xmlSerializer = new MyXmlSerializer(); |
| | 98 | | var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowe |
| | 99 | | var migrationOptions = File.Exists(migrationConfigPath) |
| | 100 | | ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! |
| | 101 | | : new MigrationOptions(); |
| | 102 | |
|
| | 103 | | // We have to deserialize it manually since the configuration manager may overwrite it |
| | 104 | | var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath) |
| | 105 | | ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemCon |
| | 106 | | : new ServerConfiguration(); |
| | 107 | |
|
| | 108 | | HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger); |
| | 109 | | await PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migr |
| | 110 | | } |
| | 111 | |
|
| | 112 | | private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions mig |
| | 113 | | { |
| 0 | 114 | | if (isStartWizardCompleted) |
| | 115 | | { |
| 0 | 116 | | return; |
| | 117 | | } |
| | 118 | |
|
| | 119 | | // If startup wizard is not finished, this is a fresh install. |
| 0 | 120 | | var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray(); |
| 0 | 121 | | logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldIns |
| 0 | 122 | | migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name))); |
| 0 | 123 | | } |
| | 124 | |
|
| | 125 | | private static async Task PerformMigrations( |
| | 126 | | IMigrationRoutine[] migrations, |
| | 127 | | MigrationOptions migrationOptions, |
| | 128 | | Action<MigrationOptions> saveConfiguration, |
| | 129 | | ILogger logger, |
| | 130 | | IJellyfinDatabaseProvider? jellyfinDatabaseProvider) |
| | 131 | | { |
| | 132 | | // save already applied migrations, and skip them thereafter |
| | 133 | | saveConfiguration(migrationOptions); |
| | 134 | | var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); |
| | 135 | | var migrationsToBeApplied = migrations.Where(e => !appliedMigrationIds.Contains(e.Id)).ToArray(); |
| | 136 | |
|
| | 137 | | string? migrationKey = null; |
| | 138 | | if (jellyfinDatabaseProvider is not null && migrationsToBeApplied.Any(f => f is IDatabaseMigrationRoutine)) |
| | 139 | | { |
| | 140 | | logger.LogInformation("Performing database backup"); |
| | 141 | | try |
| | 142 | | { |
| | 143 | | migrationKey = await jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureA |
| | 144 | | logger.LogInformation("Database backup with key '{BackupKey}' has been successfully created.", migra |
| | 145 | | } |
| | 146 | | catch (NotImplementedException) |
| | 147 | | { |
| | 148 | | logger.LogWarning("Could not perform backup of database before migration because provider does not s |
| | 149 | | } |
| | 150 | | } |
| | 151 | |
|
| | 152 | | List<IMigrationRoutine> databaseMigrations = []; |
| | 153 | | try |
| | 154 | | { |
| | 155 | | foreach (var migrationRoutine in migrationsToBeApplied) |
| | 156 | | { |
| | 157 | | logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); |
| | 158 | | var isDbMigration = migrationRoutine is IDatabaseMigrationRoutine; |
| | 159 | |
|
| | 160 | | if (isDbMigration) |
| | 161 | | { |
| | 162 | | databaseMigrations.Add(migrationRoutine); |
| | 163 | | } |
| | 164 | |
|
| | 165 | | try |
| | 166 | | { |
| | 167 | | migrationRoutine.Perform(); |
| | 168 | | } |
| | 169 | | catch (Exception ex) |
| | 170 | | { |
| | 171 | | logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); |
| | 172 | | throw; |
| | 173 | | } |
| | 174 | |
|
| | 175 | | // Mark the migration as completed |
| | 176 | | logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); |
| | 177 | | if (!isDbMigration) |
| | 178 | | { |
| | 179 | | migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); |
| | 180 | | saveConfiguration(migrationOptions); |
| | 181 | | logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name) |
| | 182 | | } |
| | 183 | | } |
| | 184 | | } |
| | 185 | | catch (Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null) |
| | 186 | | { |
| | 187 | | if (databaseMigrations.Count != 0) |
| | 188 | | { |
| | 189 | | logger.LogInformation("Rolling back database as migrations reported failure."); |
| | 190 | | await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwai |
| | 191 | | } |
| | 192 | |
|
| | 193 | | throw; |
| | 194 | | } |
| | 195 | |
|
| | 196 | | foreach (var migrationRoutine in databaseMigrations) |
| | 197 | | { |
| | 198 | | migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); |
| | 199 | | saveConfiguration(migrationOptions); |
| | 200 | | logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); |
| | 201 | | } |
| | 202 | | } |
| | 203 | | } |
| | 204 | | } |