< Summary - Jellyfin

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

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
GetJellyfinVersion()100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Reflection;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Emby.Server.Implementations.Serialization;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Server.Migrations.Stages;
 11using MediaBrowser.Common.Configuration;
 12using MediaBrowser.Model.Configuration;
 13using Microsoft.EntityFrameworkCore;
 14using Microsoft.EntityFrameworkCore.Infrastructure;
 15using Microsoft.EntityFrameworkCore.Migrations;
 16using Microsoft.Extensions.DependencyInjection;
 17using Microsoft.Extensions.Logging;
 18
 19namespace Jellyfin.Server.Migrations;
 20
 21/// <summary>
 22/// Handles Migration of the Jellyfin data structure.
 23/// </summary>
 24internal class JellyfinMigrationService
 25{
 26    private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
 27    private readonly ILoggerFactory _loggerFactory;
 28
 29    /// <summary>
 30    /// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
 31    /// </summary>
 32    /// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
 33    /// <param name="loggerFactory">The logger factory.</param>
 34    public JellyfinMigrationService(IDbContextFactory<JellyfinDbContext> dbContextFactory, ILoggerFactory loggerFactory)
 35    {
 6336        _dbContextFactory = dbContextFactory;
 6337        _loggerFactory = loggerFactory;
 38#pragma warning disable CS0618 // Type or member is obsolete
 6339        Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignable
 6340            .Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>()))
 6341            .Where(e => e.Metadata != null)
 6342            .GroupBy(e => e.Metadata!.Stage)
 6343            .Select(f =>
 6344            {
 6345                var stage = new MigrationStage(f.Key);
 6346                foreach (var item in f)
 6347                {
 6348                    stage.Add(new(item.Type, item.Metadata!));
 6349                }
 6350
 6351                return stage;
 6352            })];
 53#pragma warning restore CS0618 // Type or member is obsolete
 6354    }
 55
 56    private interface IInternalMigration
 57    {
 58        Task PerformAsync(ILogger logger);
 59    }
 60
 61    private HashSet<MigrationStage> Migrations { get; set; }
 62
 63    public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
 64    {
 65        var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
 66        logger.LogInformation("Initialise Migration service.");
 67        var xmlSerializer = new MyXmlSerializer();
 68        var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
 69            ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigu
 70            : new ServerConfiguration();
 71        if (!serverConfig.IsStartupWizardCompleted)
 72        {
 73            logger.LogInformation("System initialisation detected. Seed data.");
 74            var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray(
 75
 76            var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 77            await using (dbContext.ConfigureAwait(false))
 78            {
 79                var historyRepository = dbContext.GetService<IHistoryRepository>();
 80
 81                await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
 82                var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
 83                var startupScripts = flatApplyMigrations
 84                    .Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
 85                    .Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.Buil
 86                    .ToArray();
 87                foreach (var item in startupScripts)
 88                {
 89                    logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
 90                    await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
 91                }
 92            }
 93
 94            logger.LogInformation("Migration system initialisation completed.");
 95        }
 96        else
 97        {
 98            // migrate any existing migration.xml files
 99            var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml");
 100            var migrationOptions = File.Exists(migrationConfigPath)
 101                 ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
 102                 : null;
 103            if (migrationOptions != null && migrationOptions.Applied.Count > 0)
 104            {
 105                logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
 106                var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 107                await using (dbContext.ConfigureAwait(false))
 108                {
 109                    var historyRepository = dbContext.GetService<IHistoryRepository>();
 110                    var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
 111                    var oldMigrations = Migrations
 112                        .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have
 113                        .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
 114                        .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
 115                        .ToArray();
 116                    var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.Get
 117                    foreach (var item in startupScripts)
 118                    {
 119                        logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name
 120                        await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
 121                    }
 122
 123                    logger.LogInformation("Rename old migration.xml to migration.xml.backup");
 124                    File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
 125                }
 126            }
 127        }
 128    }
 129
 130    public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
 131    {
 132        var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
 133        logger.LogInformation("Migrate stage {Stage}.", stage);
 134        ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<Cod
 135
 136        var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 137        await using (dbContext.ConfigureAwait(false))
 138        {
 139            var historyRepository = dbContext.GetService<IHistoryRepository>();
 140            var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
 141            var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
 142            var pendingCodeMigrations = migrationStage
 143                .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
 144                .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, db
 145                .ToArray();
 146
 147            (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
 148            if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
 149            {
 150                pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.Migrat
 151                   .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
 152                   .ToArray();
 153            }
 154
 155            (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDataba
 156            logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, sta
 157            var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
 158            foreach (var item in migrations)
 159            {
 160                try
 161                {
 162                    logger.LogInformation("Perform migration {Name}", item.Key);
 163                    await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(f
 164                    logger.LogInformation("Migration {Name} was successfully applied", item.Key);
 165                }
 166                catch (Exception ex)
 167                {
 168                    logger.LogCritical(ex, "Migration {Name} failed", item.Key);
 169                    throw;
 170                }
 171            }
 172        }
 173    }
 174
 175    private static string GetJellyfinVersion()
 176    {
 546177        return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
 178    }
 179
 180    private class InternalCodeMigration : IInternalMigration
 181    {
 182        private readonly CodeMigration _codeMigration;
 183        private readonly IServiceProvider? _serviceProvider;
 184        private JellyfinDbContext _dbContext;
 185
 186        public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext d
 187        {
 105188            _codeMigration = codeMigration;
 105189            _serviceProvider = serviceProvider;
 105190            _dbContext = dbContext;
 105191        }
 192
 193        public async Task PerformAsync(ILogger logger)
 194        {
 195            await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false);
 196
 197            var historyRepository = _dbContext.GetService<IHistoryRepository>();
 198            var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), G
 199            await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
 200        }
 201    }
 202
 203    private class InternalDatabaseMigration : IInternalMigration
 204    {
 205        private readonly JellyfinDbContext _jellyfinDbContext;
 206        private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
 207
 208        public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfi
 209        {
 651210            _databaseMigrationInfo = databaseMigrationInfo;
 651211            _jellyfinDbContext = jellyfinDbContext;
 651212        }
 213
 214        public async Task PerformAsync(ILogger logger)
 215        {
 216            var migrator = _jellyfinDbContext.GetService<IMigrator>();
 217            await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
 218        }
 219    }
 220}