diff options
Diffstat (limited to 'Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs')
| -rw-r--r-- | Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs | 152 |
1 files changed, 81 insertions, 71 deletions
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 0e647fd24..e266d5a3b 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.SystemBackupService; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.FullSystemBackup; @@ -31,7 +32,7 @@ public class BackupService : IBackupService private readonly IServerApplicationHost _applicationHost; private readonly IServerApplicationPaths _applicationPaths; private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; - private readonly ISystemManager _systemManager; + private readonly IHostApplicationLifetime _hostApplicationLifetime; private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General) { AllowTrailingCommas = true, @@ -48,21 +49,21 @@ public class BackupService : IBackupService /// <param name="applicationHost">The Application host.</param> /// <param name="applicationPaths">The application paths.</param> /// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</param> - /// <param name="systemManager">The SystemManager.</param> + /// <param name="applicationLifetime">The SystemManager.</param> public BackupService( ILogger<BackupService> logger, IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost applicationHost, IServerApplicationPaths applicationPaths, IJellyfinDatabaseProvider jellyfinDatabaseProvider, - ISystemManager systemManager) + IHostApplicationLifetime applicationLifetime) { _logger = logger; _dbProvider = dbProvider; _applicationHost = applicationHost; _applicationPaths = applicationPaths; _jellyfinDatabaseProvider = jellyfinDatabaseProvider; - _systemManager = systemManager; + _hostApplicationLifetime = applicationLifetime; } /// <inheritdoc/> @@ -71,7 +72,11 @@ public class BackupService : IBackupService _applicationHost.RestoreBackupPath = archivePath; _applicationHost.ShouldRestart = true; _applicationHost.NotifyPendingRestart(); - _systemManager.Restart(); + _ = Task.Run(async () => + { + await Task.Delay(500).ConfigureAwait(false); + _hostApplicationLifetime.StopApplication(); + }); } /// <inheritdoc/> @@ -136,87 +141,90 @@ public class BackupService : IBackupService CopyDirectory(_applicationPaths.DataPath, "Data/"); CopyDirectory(_applicationPaths.RootFolderPath, "Root/"); - _logger.LogInformation("Begin restoring Database"); - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + if (manifest.Options.Database) { - // restore migration history manually - var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json"); - if (historyEntry is null) - { - _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation"); - throw new InvalidOperationException("Cannot restore backup that has no History data."); - } - - HistoryRow[] historyEntries; - var historyArchive = historyEntry.Open(); - await using (historyArchive.ConfigureAwait(false)) + _logger.LogInformation("Begin restoring Database"); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ?? + // restore migration history manually + var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json"); + if (historyEntry is null) + { + _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation"); throw new InvalidOperationException("Cannot restore backup that has no History data."); - } + } - var historyRepository = dbContext.GetService<IHistoryRepository>(); - await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false); - foreach (var item in historyEntries) - { - var insertScript = historyRepository.GetInsertScript(item); - await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false); - } + HistoryRow[] historyEntries; + var historyArchive = historyEntry.Open(); + await using (historyArchive.ConfigureAwait(false)) + { + historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ?? + throw new InvalidOperationException("Cannot restore backup that has no History data."); + } - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) - .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable)) - .ToArray(); + var historyRepository = dbContext.GetService<IHistoryRepository>(); + await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false); + foreach (var item in historyEntries) + { + var insertScript = historyRepository.GetInsertScript(item); + await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false); + } - var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!); - _logger.LogInformation("Begin purging database"); - await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false); - _logger.LogInformation("Database Purged"); + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) + .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable)) + .ToArray(); - foreach (var entityType in entityTypes) - { - _logger.LogInformation("Read backup of {Table}", entityType.Type.Name); + var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!); + _logger.LogInformation("Begin purging database"); + await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false); + _logger.LogInformation("Database Purged"); - var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json"); - if (zipEntry is null) + foreach (var entityType in entityTypes) { - _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name); - continue; - } + _logger.LogInformation("Read backup of {Table}", entityType.Type.Name); - var zipEntryStream = zipEntry.Open(); - await using (zipEntryStream.ConfigureAwait(false)) - { - _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name); - var records = 0; - await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!) + var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json"); + if (zipEntry is null) { - var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]); - if (entity is null) - { - throw new InvalidOperationException($"Cannot deserialize entity '{item}'"); - } + _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name); + continue; + } - try - { - records++; - dbContext.Add(entity); - } - catch (Exception ex) + var zipEntryStream = zipEntry.Open(); + await using (zipEntryStream.ConfigureAwait(false)) + { + _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name); + var records = 0; + await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!) { - _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item); + var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]); + if (entity is null) + { + throw new InvalidOperationException($"Cannot deserialize entity '{item}'"); + } + + try + { + records++; + dbContext.Add(entity); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item); + } } - } - _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityType.Type.Name); + _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityType.Type.Name); + } } - } - _logger.LogInformation("Try restore Database"); - await dbContext.SaveChangesAsync().ConfigureAwait(false); - _logger.LogInformation("Restored database."); + _logger.LogInformation("Try restore Database"); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + _logger.LogInformation("Restored database."); + } } _logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated); @@ -486,7 +494,8 @@ public class BackupService : IBackupService { Metadata = options.Metadata, Subtitles = options.Subtitles, - Trickplay = options.Trickplay + Trickplay = options.Trickplay, + Database = options.Database }; } @@ -496,7 +505,8 @@ public class BackupService : IBackupService { Metadata = options.Metadata, Subtitles = options.Subtitles, - Trickplay = options.Trickplay + Trickplay = options.Trickplay, + Database = options.Database }; } } |
