JellyfinMigrationService.cs 23 KB

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