< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.MigrationRunner
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/MigrationRunner.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 37
Coverable lines: 37
Total lines: 204
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 2
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

0255075100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
HandleStartupWizardCondition(...)0%620%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/MigrationRunner.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using Emby.Server.Implementations;
 8using Emby.Server.Implementations.Serialization;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Server.Implementations;
 11using MediaBrowser.Common.Configuration;
 12using MediaBrowser.Model.Configuration;
 13using Microsoft.EntityFrameworkCore.Storage;
 14using Microsoft.Extensions.DependencyInjection;
 15using Microsoft.Extensions.Logging;
 16
 17namespace 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>
 027        private static readonly Type[] _preStartupMigrationTypes =
 028        {
 029            typeof(PreStartupRoutines.CreateNetworkConfiguration),
 030            typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
 031            typeof(PreStartupRoutines.MigrateNetworkConfiguration),
 032            typeof(PreStartupRoutines.MigrateEncodingOptions),
 033            typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections)
 034        };
 35
 36        /// <summary>
 37        /// The list of known migrations, in order of applicability.
 38        /// </summary>
 039        private static readonly Type[] _migrationTypes =
 040        {
 041            typeof(Routines.DisableTranscodingThrottling),
 042            typeof(Routines.CreateUserLoggingConfigFile),
 043            typeof(Routines.MigrateActivityLogDb),
 044            typeof(Routines.RemoveDuplicateExtras),
 045            typeof(Routines.AddDefaultPluginRepository),
 046            typeof(Routines.MigrateUserDb),
 047            typeof(Routines.ReaddDefaultPluginRepository),
 048            typeof(Routines.MigrateDisplayPreferencesDb),
 049            typeof(Routines.RemoveDownloadImagesInAdvance),
 050            typeof(Routines.MigrateAuthenticationDb),
 051            typeof(Routines.FixPlaylistOwner),
 052            typeof(Routines.AddDefaultCastReceivers),
 053            typeof(Routines.UpdateDefaultPluginRepository),
 054            typeof(Routines.FixAudioData),
 055            typeof(Routines.RemoveDuplicatePlaylistChildren),
 056            typeof(Routines.MigrateLibraryDb),
 057            typeof(Routines.MoveExtractedFiles),
 058            typeof(Routines.MigrateRatingLevels),
 059            typeof(Routines.MoveTrickplayFiles),
 060            typeof(Routines.MigrateKeyframeData),
 061        };
 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        {
 0114            if (isStartWizardCompleted)
 115            {
 0116                return;
 117            }
 118
 119            // If startup wizard is not finished, this is a fresh install.
 0120            var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray();
 0121            logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldIns
 0122            migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name)));
 0123        }
 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}