JellyfinMigrationService.cs 11 KB

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