From ed6cb30762dbeb018209e19a8896059e4f906c1b Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 27 Oct 2025 15:43:20 -0400 Subject: [PATCH] Backport pull request #15170 from jellyfin/release-10.11.z Clean up BackupService Original-merge: ac3fa3c376a47c099e14d4b940832c39e2249aee Merged-by: crobibero Backported-by: Bond_009 --- .../FullSystemBackup/BackupService.cs | 236 ++++++++++-------- 1 file changed, 131 insertions(+), 105 deletions(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index e5c3cef3d3..e39a2b42f6 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -199,7 +199,7 @@ public class BackupService : IBackupService var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json"))); if (zipEntry is null) { - _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name); + _logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name); continue; } @@ -223,7 +223,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item); + _logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item); } } @@ -233,11 +233,11 @@ public class BackupService : IBackupService _logger.LogInformation("Try restore Database"); await dbContext.SaveChangesAsync().ConfigureAwait(false); - _logger.LogInformation("Restored database."); + _logger.LogInformation("Restored database"); } } - _logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated); + _logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated); } } @@ -263,6 +263,8 @@ public class BackupService : IBackupService Options = Map(backupOptions) }; + _logger.LogInformation("Running database optimization before backup"); + await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false); var backupFolder = Path.Combine(_applicationPaths.BackupPath); @@ -281,130 +283,154 @@ public class BackupService : IBackupService } var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip"); - _logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath); - var fileStream = File.OpenWrite(backupPath); - await using (fileStream.ConfigureAwait(false)) - using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) + + try { - _logger.LogInformation("Start backup process."); - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + _logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath); + var fileStream = File.OpenWrite(backupPath); + await using (fileStream.ConfigureAwait(false)) + using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) { - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - static IAsyncEnumerable GetValues(IQueryable dbSet) + _logger.LogInformation("Starting backup process"); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - var method = dbSet.GetType().GetMethod(nameof(DbSet.AsAsyncEnumerable))!; - var enumerable = method.Invoke(dbSet, null)!; - return (IAsyncEnumerable)enumerable; - } + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - // include the migration history as well - var historyRepository = dbContext.GetService(); - var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false); - - ICollection<(Type Type, string SourceName, Func> ValueFactory)> entityTypes = [ - .. typeof(JellyfinDbContext) - .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) - .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))), - (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable()) - ]; - manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray(); - var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false); - - await using (transaction.ConfigureAwait(false)) - { - _logger.LogInformation("Begin Database backup"); - - foreach (var entityType in entityTypes) + static IAsyncEnumerable GetValues(IQueryable dbSet) { - _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName); - var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json"))); - var entities = 0; - var zipEntryStream = zipEntry.Open(); - await using (zipEntryStream.ConfigureAwait(false)) + var method = dbSet.GetType().GetMethod(nameof(DbSet.AsAsyncEnumerable))!; + var enumerable = method.Invoke(dbSet, null)!; + return (IAsyncEnumerable)enumerable; + } + + // include the migration history as well + var historyRepository = dbContext.GetService(); + var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false); + + ICollection<(Type Type, string SourceName, Func> ValueFactory)> entityTypes = + [ + .. typeof(JellyfinDbContext) + .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) + .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))), + (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable()) + ]; + manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray(); + var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false); + + await using (transaction.ConfigureAwait(false)) + { + _logger.LogInformation("Begin Database backup"); + + foreach (var entityType in entityTypes) { - var jsonSerializer = new Utf8JsonWriter(zipEntryStream); - await using (jsonSerializer.ConfigureAwait(false)) + _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName); + var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json"))); + var entities = 0; + var zipEntryStream = zipEntry.Open(); + await using (zipEntryStream.ConfigureAwait(false)) { - jsonSerializer.WriteStartArray(); - - var set = entityType.ValueFactory().ConfigureAwait(false); - await foreach (var item in set.ConfigureAwait(false)) + var jsonSerializer = new Utf8JsonWriter(zipEntryStream); + await using (jsonSerializer.ConfigureAwait(false)) { - entities++; - try + jsonSerializer.WriteStartArray(); + + var set = entityType.ValueFactory().ConfigureAwait(false); + await foreach (var item in set.ConfigureAwait(false)) { - JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not load entity {Entity}", item); - throw; + entities++; + try + { + using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings); + document.WriteTo(jsonSerializer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not load entity {Entity}", item); + throw; + } } + + jsonSerializer.WriteEndArray(); } - - jsonSerializer.WriteEndArray(); } - } - _logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities); + _logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities); + } } } - } - _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath); - foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly) - .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly))) - { - zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))); - } - - void CopyDirectory(string source, string target, string filter = "*") - { - if (!Directory.Exists(source)) + _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath); + foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly) + .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly))) { - return; + zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))); } - _logger.LogInformation("Backup of folder {Table}", source); - - foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) + void CopyDirectory(string source, string target, string filter = "*") { - zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); + if (!Directory.Exists(source)) + { + return; + } + + _logger.LogInformation("Backup of folder {Table}", source); + + foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) + { + zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); + } + } + + CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users")); + CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks")); + CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root"); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections")); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists")); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks")); + if (backupOptions.Subtitles) + { + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles")); + } + + if (backupOptions.Trickplay) + { + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay")); + } + + if (backupOptions.Metadata) + { + CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); + } + + var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); + await using (manifestStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); } } - CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users")); - CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks")); - CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root"); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections")); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists")); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks")); - if (backupOptions.Subtitles) - { - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles")); - } - - if (backupOptions.Trickplay) - { - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay")); - } - - if (backupOptions.Metadata) - { - CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); - } - - var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); - await using (manifestStream.ConfigureAwait(false)) - { - await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); - } + _logger.LogInformation("Backup created"); + return Map(manifest, backupPath); } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath); + try + { + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + } + catch (Exception innerEx) + { + _logger.LogWarning(innerEx, "Unable to remove failed backup"); + } - _logger.LogInformation("Backup created"); - return Map(manifest, backupPath); + throw; + } } /// @@ -422,7 +448,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath); + _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath); return null; } @@ -459,7 +485,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Could not load {BackupArchive} path.", item); + _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item); } }