|
|
@@ -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);
|
|
|
}
|
|
|
}
|
|
|
|