123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Threading;
- using System.Threading.Tasks;
- using Emby.Server.Implementations.Serialization;
- using Jellyfin.Database.Implementations;
- using Jellyfin.Server.Implementations.SystemBackupService;
- using Jellyfin.Server.Migrations.Stages;
- using MediaBrowser.Common.Configuration;
- using MediaBrowser.Controller.SystemBackupService;
- using MediaBrowser.Model.Configuration;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.EntityFrameworkCore.Infrastructure;
- using Microsoft.EntityFrameworkCore.Migrations;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- namespace Jellyfin.Server.Migrations;
- /// <summary>
- /// Handles Migration of the Jellyfin data structure.
- /// </summary>
- internal class JellyfinMigrationService
- {
- private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
- private readonly ILoggerFactory _loggerFactory;
- /// <summary>
- /// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
- /// </summary>
- /// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
- /// <param name="loggerFactory">The logger factory.</param>
- public JellyfinMigrationService(IDbContextFactory<JellyfinDbContext> dbContextFactory, ILoggerFactory loggerFactory)
- {
- _dbContextFactory = dbContextFactory;
- _loggerFactory = loggerFactory;
- #pragma warning disable CS0618 // Type or member is obsolete
- Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e))
- .Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>()))
- .Where(e => e.Metadata != null)
- .GroupBy(e => e.Metadata!.Stage)
- .Select(f =>
- {
- var stage = new MigrationStage(f.Key);
- foreach (var item in f)
- {
- stage.Add(new(item.Type, item.Metadata!));
- }
- return stage;
- })];
- #pragma warning restore CS0618 // Type or member is obsolete
- }
- private interface IInternalMigration
- {
- Task PerformAsync(ILogger logger);
- }
- private HashSet<MigrationStage> Migrations { get; set; }
- public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
- {
- var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
- logger.LogInformation("Initialise Migration service.");
- var xmlSerializer = new MyXmlSerializer();
- var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
- ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
- : new ServerConfiguration();
- if (!serverConfig.IsStartupWizardCompleted)
- {
- logger.LogInformation("System initialisation detected. Seed data.");
- var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray();
- var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- var historyRepository = dbContext.GetService<IHistoryRepository>();
- await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
- var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var startupScripts = flatApplyMigrations
- .Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
- .Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))))
- .ToArray();
- foreach (var item in startupScripts)
- {
- logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
- await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
- }
- }
- logger.LogInformation("Migration system initialisation completed.");
- }
- else
- {
- // migrate any existing migration.xml files
- var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml");
- var migrationOptions = File.Exists(migrationConfigPath)
- ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
- : null;
- if (migrationOptions != null && migrationOptions.Applied.Count > 0)
- {
- logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
- try
- {
- var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- var historyRepository = dbContext.GetService<IHistoryRepository>();
- var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var oldMigrations = Migrations
- .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
- .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
- .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
- .ToArray();
- var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
- foreach (var item in startupScripts)
- {
- logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
- await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
- }
- logger.LogInformation("Rename old migration.xml to migration.xml.backup");
- File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
- }
- }
- catch (Exception ex)
- {
- logger.LogCritical(ex, "Failed to apply migrations");
- throw;
- }
- }
- }
- }
- public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
- {
- var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
- logger.LogInformation("Migrate stage {Stage}.", stage);
- ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<CodeMigration>) ?? [];
- var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- var historyRepository = dbContext.GetService<IHistoryRepository>();
- var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
- var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var pendingCodeMigrations = migrationStage
- .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
- .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
- .ToArray();
- (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
- if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
- {
- pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
- .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
- .ToArray();
- }
- (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
- logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
- var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
- foreach (var item in migrations)
- {
- try
- {
- logger.LogInformation("Perform migration {Name}", item.Key);
- await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(false);
- logger.LogInformation("Migration {Name} was successfully applied", item.Key);
- }
- catch (Exception ex)
- {
- logger.LogCritical(ex, "Migration {Name} failed", item.Key);
- throw;
- }
- }
- }
- }
- private static string GetJellyfinVersion()
- {
- return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
- }
- private class InternalCodeMigration : IInternalMigration
- {
- private readonly CodeMigration _codeMigration;
- private readonly IServiceProvider? _serviceProvider;
- private JellyfinDbContext _dbContext;
- public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext dbContext)
- {
- _codeMigration = codeMigration;
- _serviceProvider = serviceProvider;
- _dbContext = dbContext;
- }
- public async Task PerformAsync(ILogger logger)
- {
- await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false);
- var historyRepository = _dbContext.GetService<IHistoryRepository>();
- var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()));
- await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
- }
- }
- private class InternalDatabaseMigration : IInternalMigration
- {
- private readonly JellyfinDbContext _jellyfinDbContext;
- private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
- public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfinDbContext)
- {
- _databaseMigrationInfo = databaseMigrationInfo;
- _jellyfinDbContext = jellyfinDbContext;
- }
- public async Task PerformAsync(ILogger logger)
- {
- var migrator = _jellyfinDbContext.GetService<IMigrator>();
- await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
- }
- }
- }
|