JellyfinMigrationService.cs 21 KB

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