< 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: 459
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

Coverage history 0 25 50 75 100

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