JellyfinMigrationService.cs 11 KB

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