< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.JellyfinMigrationService
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
Line coverage
81%
Covered lines: 35
Uncovered lines: 8
Coverable lines: 43
Total lines: 451
Line coverage: 81.3%
Branch coverage
16%
Covered branches: 2
Total branches: 12
Branch coverage: 16.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

0255075100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
GetJellyfinVersion()100%11100%
MergeBackupAttributes(...)0%110100%
.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.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Reflection;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using Emby.Server.Implementations.Serialization;
 10using Jellyfin.Database.Implementations;
 11using Jellyfin.Server.Implementations.SystemBackupService;
 12using Jellyfin.Server.Migrations.Stages;
 13using Jellyfin.Server.ServerSetupApp;
 14using MediaBrowser.Common.Configuration;
 15using MediaBrowser.Controller.SystemBackupService;
 16using MediaBrowser.Model.Configuration;
 17using Microsoft.EntityFrameworkCore;
 18using Microsoft.EntityFrameworkCore.Infrastructure;
 19using Microsoft.EntityFrameworkCore.Migrations;
 20using Microsoft.Extensions.Logging;
 21
 22namespace Jellyfin.Server.Migrations;
 23
 24/// <summary>
 25/// Handles Migration of the Jellyfin data structure.
 26/// </summary>
 27internal class JellyfinMigrationService
 28{
 29    private const string DbFilename = "library.db";
 30    private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
 31    private readonly ILoggerFactory _loggerFactory;
 32    private readonly IStartupLogger _startupLogger;
 33    private readonly IBackupService? _backupService;
 34    private readonly IJellyfinDatabaseProvider? _jellyfinDatabaseProvider;
 35    private readonly IApplicationPaths _applicationPaths;
 36    private (string? LibraryDb, string? JellyfinDb, BackupManifestDto? FullBackup) _backupKey;
 37
 38    /// <summary>
 39    /// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
 40    /// </summary>
 41    /// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
 42    /// <param name="loggerFactory">The logger factory.</param>
 43    /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
 44    /// <param name="applicationPaths">Application paths for library.db backup.</param>
 45    /// <param name="backupService">The jellyfin backup service.</param>
 46    /// <param name="jellyfinDatabaseProvider">The jellyfin database provider.</param>
 47    public JellyfinMigrationService(
 48        IDbContextFactory<JellyfinDbContext> dbContextFactory,
 49        ILoggerFactory loggerFactory,
 50        IStartupLogger<JellyfinMigrationService> startupLogger,
 51        IApplicationPaths applicationPaths,
 52        IBackupService? backupService = null,
 53        IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null)
 54    {
 6355        _dbContextFactory = dbContextFactory;
 6356        _loggerFactory = loggerFactory;
 6357        _startupLogger = startupLogger;
 6358        _backupService = backupService;
 6359        _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 6360        _applicationPaths = applicationPaths;
 61#pragma warning disable CS0618 // Type or member is obsolete
 6362        Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignable
 6363            .Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>(), Backup: e.GetCustomAttr
 6364            .Where(e => e.Metadata != null)
 6365            .GroupBy(e => e.Metadata!.Stage)
 6366            .Select(f =>
 6367            {
 6368                var stage = new MigrationStage(f.Key);
 6369                foreach (var item in f)
 6370                {
 6371                    JellyfinMigrationBackupAttribute? backupMetadata = null;
 6372                    if (item.Backup?.Any() == true)
 6373                    {
 6374                        backupMetadata = item.Backup.Aggregate(MergeBackupAttributes);
 6375                    }
 6376
 6377                    stage.Add(new(item.Type, item.Metadata!, backupMetadata));
 6378                }
 6379
 6380                return stage;
 6381            })];
 82#pragma warning restore CS0618 // Type or member is obsolete
 6383    }
 84
 85    private interface IInternalMigration
 86    {
 87        Task PerformAsync(IStartupLogger logger);
 88    }
 89
 90    private HashSet<MigrationStage> Migrations { get; set; }
 91
 92    public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
 93    {
 94        var logger = _startupLogger.With(_loggerFactory.CreateLogger<JellyfinMigrationService>()).BeginGroup($"Migration
 95        logger.LogInformation("Initialise Migration service.");
 96        var xmlSerializer = new MyXmlSerializer();
 97        var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
 98            ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigu
 99            : new ServerConfiguration();
 100        if (!serverConfig.IsStartupWizardCompleted)
 101        {
 102            logger.LogInformation("System initialisation detected. Seed data.");
 103            var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray(
 104
 105            var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 106            await using (dbContext.ConfigureAwait(false))
 107            {
 108                var historyRepository = dbContext.GetService<IHistoryRepository>();
 109
 110                await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
 111                var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
 112                var startupScripts = flatApplyMigrations
 113                    .Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
 114                    .Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.Buil
 115                    .ToArray();
 116                foreach (var item in startupScripts)
 117                {
 118                    logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
 119                    await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
 120                }
 121            }
 122
 123            logger.LogInformation("Migration system initialisation completed.");
 124        }
 125        else
 126        {
 127            // migrate any existing migration.xml files
 128            var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml");
 129            var migrationOptions = File.Exists(migrationConfigPath)
 130                 ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
 131                 : null;
 132            if (migrationOptions != null && migrationOptions.Applied.Count > 0)
 133            {
 134                logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
 135                try
 136                {
 137                    var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 138                    await using (dbContext.ConfigureAwait(false))
 139                    {
 140                        var historyRepository = dbContext.GetService<IHistoryRepository>();
 141                        var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(fals
 142                        var lastOldAppliedMigration = Migrations
 143                            .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that 
 144                            .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
 145                            .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
 146                            .OrderBy(e => e.BuildCodeMigrationId())
 147                            .Last(); // this is the latest migration applied in the old migration.xml
 148
 149                        IReadOnlyList<CodeMigration> oldMigrations = [
 150                            .. Migrations
 151                            .SelectMany(e => e)
 152                            .OrderBy(e => e.BuildCodeMigrationId())
 153                            .TakeWhile(e => e.BuildCodeMigrationId() != lastOldAppliedMigration.BuildCodeMigrationId()),
 154                            lastOldAppliedMigration
 155                        ];
 156                        // those are all migrations that had to run in the old migration system, even if not noted in th
 157
 158                        var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository
 159                        foreach (var item in startupScripts)
 160                        {
 161                            logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.
 162                            await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
 163                        }
 164
 165                        logger.LogInformation("Rename old migration.xml to migration.xml.backup");
 166                        File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
 167                    }
 168                }
 169                catch (Exception ex)
 170                {
 171                    logger.LogCritical(ex, "Failed to apply migrations");
 172                    throw;
 173                }
 174            }
 175        }
 176    }
 177
 178    public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
 179    {
 180        var logger = _startupLogger.With(_loggerFactory.CreateLogger<JellyfinMigrationService>()).BeginGroup($"Migrate s
 181        ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<Cod
 182
 183        var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 184        await using (dbContext.ConfigureAwait(false))
 185        {
 186            var historyRepository = dbContext.GetService<IHistoryRepository>();
 187            var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
 188            var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
 189            var pendingCodeMigrations = migrationStage
 190                .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
 191                .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, db
 192                .ToArray();
 193
 194            (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
 195            if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
 196            {
 197                pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.Migrat
 198                   .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
 199                   .ToArray();
 200            }
 201
 202            (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDataba
 203            logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, sta
 204            var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
 205
 206            foreach (var item in migrations)
 207            {
 208                var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup
 209                try
 210                {
 211                    migrationLogger.LogInformation("Perform migration {Name}", item.Key);
 212                    await item.Migration.PerformAsync(migrationLogger).ConfigureAwait(false);
 213                    migrationLogger.LogInformation("Migration {Name} was successfully applied", item.Key);
 214                }
 215                catch (Exception ex)
 216                {
 217                    migrationLogger.LogCritical("Error: {Error}", ex.Message);
 218                    migrationLogger.LogError(ex, "Migration {Name} failed", item.Key);
 219
 220                    if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
 221                    {
 222                        if (_backupKey.LibraryDb is not null)
 223                        {
 224                            migrationLogger.LogInformation("Attempt to rollback librarydb.");
 225                            try
 226                            {
 227                                var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
 228                                File.Move(_backupKey.LibraryDb, libraryDbPath, true);
 229                            }
 230                            catch (Exception inner)
 231                            {
 232                                migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual interventio
 233                            }
 234                        }
 235
 236                        if (_backupKey.JellyfinDb is not null)
 237                        {
 238                            migrationLogger.LogInformation("Attempt to rollback JellyfinDb.");
 239                            try
 240                            {
 241                                await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationTok
 242                            }
 243                            catch (Exception inner)
 244                            {
 245                                migrationLogger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual interventio
 246                            }
 247                        }
 248
 249                        if (_backupKey.FullBackup is not null)
 250                        {
 251                            migrationLogger.LogInformation("Attempt to rollback from backup.");
 252                            try
 253                            {
 254                                await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false
 255                            }
 256                            catch (Exception inner)
 257                            {
 258                                migrationLogger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual inte
 259                            }
 260                        }
 261                    }
 262
 263                    throw;
 264                }
 265            }
 266        }
 267    }
 268
 269    private static string GetJellyfinVersion()
 270    {
 567271        return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
 272    }
 273
 274    public async Task CleanupSystemAfterMigration(ILogger logger)
 275    {
 276        if (_backupKey != default)
 277        {
 278            if (_backupKey.LibraryDb is not null)
 279            {
 280                logger.LogInformation("Attempt to cleanup librarydb backup.");
 281                try
 282                {
 283                    File.Delete(_backupKey.LibraryDb);
 284                }
 285                catch (Exception inner)
 286                {
 287                    logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.LibraryDb);
 288                }
 289            }
 290
 291            if (_backupKey.JellyfinDb is not null && _jellyfinDatabaseProvider is not null)
 292            {
 293                logger.LogInformation("Attempt to cleanup JellyfinDb backup.");
 294                try
 295                {
 296                    await _jellyfinDatabaseProvider.DeleteBackup(_backupKey.JellyfinDb).ConfigureAwait(false);
 297                }
 298                catch (Exception inner)
 299                {
 300                    logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.JellyfinDb);
 301                }
 302            }
 303
 304            if (_backupKey.FullBackup is not null)
 305            {
 306                logger.LogInformation("Attempt to cleanup from migration backup.");
 307                try
 308                {
 309                    File.Delete(_backupKey.FullBackup.Path);
 310                }
 311                catch (Exception inner)
 312                {
 313                    logger.LogCritical(inner, "Could not cleanup backup {Backup}.", _backupKey.FullBackup.Path);
 314                }
 315            }
 316        }
 317    }
 318
 319    public async Task PrepareSystemForMigration(ILogger logger)
 320    {
 321        logger.LogInformation("Prepare system for possible migrations");
 322        JellyfinMigrationBackupAttribute backupInstruction;
 323        IReadOnlyList<HistoryRow> appliedMigrations;
 324        var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
 325        await using (dbContext.ConfigureAwait(false))
 326        {
 327            var historyRepository = dbContext.GetService<IHistoryRepository>();
 328            var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
 329            appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
 330            backupInstruction = new JellyfinMigrationBackupAttribute()
 331            {
 332                JellyfinDb = migrationsAssembly.Migrations.Any(f => appliedMigrations.All(e => e.MigrationId != f.Key))
 333            };
 334        }
 335
 336        backupInstruction = Migrations.SelectMany(e => e)
 337           .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
 338           .Select(e => e.BackupRequirements)
 339           .Where(e => e is not null)
 340           .Aggregate(backupInstruction, MergeBackupAttributes!);
 341
 342        if (backupInstruction.LegacyLibraryDb)
 343        {
 344            logger.LogInformation("A migration will attempt to modify the library.db, will attempt to backup the file no
 345            // for legacy migrations that still operates on the library.db
 346            var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
 347            if (File.Exists(libraryDbPath))
 348            {
 349                for (int i = 1; ; i++)
 350                {
 351                    var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", libraryDbPath, i);
 352                    if (!File.Exists(bakPath))
 353                    {
 354                        try
 355                        {
 356                            logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
 357                            File.Copy(libraryDbPath, bakPath);
 358                            _backupKey = (bakPath, _backupKey.JellyfinDb, _backupKey.FullBackup);
 359                            logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
 360                            break;
 361                        }
 362                        catch (Exception ex)
 363                        {
 364                            logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, ba
 365                            throw;
 366                        }
 367                    }
 368                }
 369
 370                logger.LogInformation("{Library} has been backed up as {BackupPath}", DbFilename, _backupKey.LibraryDb);
 371            }
 372            else
 373            {
 374                logger.LogError("Cannot make a backup of {Library} at path {BackupPath} because file could not be found 
 375            }
 376        }
 377
 378        if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null)
 379        {
 380            logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file n
 381            _backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.No
 382            logger.LogInformation("Jellyfin database has been backed up as {BackupPath}", _backupKey.JellyfinDb);
 383        }
 384
 385        if (_backupService is not null && (backupInstruction.Metadata || backupInstruction.Subtitles || backupInstructio
 386        {
 387            logger.LogInformation("A migration will attempt to modify system resources. Will attempt to create backup no
 388            _backupKey = (_backupKey.LibraryDb, _backupKey.JellyfinDb, await _backupService.CreateBackupAsync(new Backup
 389            {
 390                Metadata = backupInstruction.Metadata,
 391                Subtitles = backupInstruction.Subtitles,
 392                Trickplay = backupInstruction.Trickplay,
 393                Database = false // database backups are explicitly handled by the provider itself as the backup service
 394            }).ConfigureAwait(false));
 395            logger.LogInformation("Pre-Migration backup successfully created as {BackupKey}", _backupKey.FullBackup.Path
 396        }
 397    }
 398
 399    private static JellyfinMigrationBackupAttribute MergeBackupAttributes(JellyfinMigrationBackupAttribute left, Jellyfi
 400    {
 0401        return new JellyfinMigrationBackupAttribute()
 0402        {
 0403            JellyfinDb = left!.JellyfinDb || right!.JellyfinDb,
 0404            LegacyLibraryDb = left.LegacyLibraryDb || right!.LegacyLibraryDb,
 0405            Metadata = left.Metadata || right!.Metadata,
 0406            Subtitles = left.Subtitles || right!.Subtitles,
 0407            Trickplay = left.Trickplay || right!.Trickplay
 0408        };
 409    }
 410
 411    private class InternalCodeMigration : IInternalMigration
 412    {
 413        private readonly CodeMigration _codeMigration;
 414        private readonly IServiceProvider? _serviceProvider;
 415        private JellyfinDbContext _dbContext;
 416
 417        public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext d
 418        {
 105419            _codeMigration = codeMigration;
 105420            _serviceProvider = serviceProvider;
 105421            _dbContext = dbContext;
 105422        }
 423
 424        public async Task PerformAsync(IStartupLogger logger)
 425        {
 426            await _codeMigration.Perform(_serviceProvider, logger, CancellationToken.None).ConfigureAwait(false);
 427
 428            var historyRepository = _dbContext.GetService<IHistoryRepository>();
 429            var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), G
 430            await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
 431        }
 432    }
 433
 434    private class InternalDatabaseMigration : IInternalMigration
 435    {
 436        private readonly JellyfinDbContext _jellyfinDbContext;
 437        private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
 438
 439        public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfi
 440        {
 672441            _databaseMigrationInfo = databaseMigrationInfo;
 672442            _jellyfinDbContext = jellyfinDbContext;
 672443        }
 444
 445        public async Task PerformAsync(IStartupLogger logger)
 446        {
 447            var migrator = _jellyfinDbContext.GetService<IMigrator>();
 448            await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
 449        }
 450    }
 451}