Browse Source

Clean up backup service (#15170)

Cody Robibero 4 days ago
parent
commit
ac3fa3c376
1 changed files with 123 additions and 97 deletions
  1. 123 97
      Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs

+ 123 - 97
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<object> 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<object>.AsAsyncEnumerable))!;
-                    var enumerable = method.Invoke(dbSet, null)!;
-                    return (IAsyncEnumerable<object>)enumerable;
-                }
+                    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 
-                // include the migration history as well
-                var historyRepository = dbContext.GetService<IHistoryRepository>();
-                var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
-
-                ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> 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<IAsyncEnumerable<object>>(() => 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");
+                    static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
+                    {
+                        var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
+                        var enumerable = method.Invoke(dbSet, null)!;
+                        return (IAsyncEnumerable<object>)enumerable;
+                    }
 
-                    foreach (var entityType in entityTypes)
+                    // include the migration history as well
+                    var historyRepository = dbContext.GetService<IHistoryRepository>();
+                    var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
+
+                    ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> 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<IAsyncEnumerable<object>>(() => 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 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))
+                        _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.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
-                                    }
-                                    catch (Exception ex)
+                                    jsonSerializer.WriteStartArray();
+
+                                    var set = entityType.ValueFactory().ConfigureAwait(false);
+                                    await foreach (var item in set.ConfigureAwait(false))
                                     {
-                                        _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))));
-            }
+                _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))
+                void CopyDirectory(string source, string target, string filter = "*")
                 {
-                    return;
+                    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))));
+                    }
                 }
 
-                _logger.LogInformation("Backup of folder {Table}", source);
+                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"));
+                }
 
-                foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
+                if (backupOptions.Trickplay)
                 {
-                    zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
+                    CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
                 }
-            }
 
-            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.Metadata)
+                {
+                    CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
+                }
 
-            if (backupOptions.Trickplay)
-            {
-                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
+                var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
+                await using (manifestStream.ConfigureAwait(false))
+                {
+                    await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
+                }
             }
 
-            if (backupOptions.Metadata)
+            _logger.LogInformation("Backup created");
+            return Map(manifest, backupPath);
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
+            try
             {
-                CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
+                if (File.Exists(backupPath))
+                {
+                    File.Delete(backupPath);
+                }
             }
-
-            var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
-            await using (manifestStream.ConfigureAwait(false))
+            catch (Exception innerEx)
             {
-                await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
+                _logger.LogWarning(innerEx, "Unable to remove failed backup");
             }
-        }
 
-        _logger.LogInformation("Backup created");
-        return Map(manifest, backupPath);
+            throw;
+        }
     }
 
     /// <inheritdoc/>
@@ -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);
             }
         }