aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs')
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs161
1 files changed, 105 insertions, 56 deletions
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
index c3f5b0103..e266d5a3b 100644
--- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
@@ -14,6 +14,9 @@ using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Controller;
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;
@@ -29,6 +32,7 @@ public class BackupService : IBackupService
private readonly IServerApplicationHost _applicationHost;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
+ private readonly IHostApplicationLifetime _hostApplicationLifetime;
private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
{
AllowTrailingCommas = true,
@@ -45,18 +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="applicationLifetime">The SystemManager.</param>
public BackupService(
ILogger<BackupService> logger,
IDbContextFactory<JellyfinDbContext> dbProvider,
IServerApplicationHost applicationHost,
IServerApplicationPaths applicationPaths,
- IJellyfinDatabaseProvider jellyfinDatabaseProvider)
+ IJellyfinDatabaseProvider jellyfinDatabaseProvider,
+ IHostApplicationLifetime applicationLifetime)
{
_logger = logger;
_dbProvider = dbProvider;
_applicationHost = applicationHost;
_applicationPaths = applicationPaths;
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
+ _hostApplicationLifetime = applicationLifetime;
}
/// <inheritdoc/>
@@ -65,6 +72,11 @@ public class BackupService : IBackupService
_applicationHost.RestoreBackupPath = archivePath;
_applicationHost.ShouldRestart = true;
_applicationHost.NotifyPendingRestart();
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(500).ConfigureAwait(false);
+ _hostApplicationLifetime.StopApplication();
+ });
}
/// <inheritdoc/>
@@ -129,63 +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)
{
- 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 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");
-
- foreach (var entityType in entityTypes)
+ _logger.LogInformation("Begin restoring Database");
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
+ // 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 zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
- if (zipEntry is null)
+ HistoryRow[] historyEntries;
+ var historyArchive = historyEntry.Open();
+ await using (historyArchive.ConfigureAwait(false))
{
- _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
- continue;
+ historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
+ throw new InvalidOperationException("Cannot restore backup that has no History data.");
}
- var zipEntryStream = zipEntry.Open();
- await using (zipEntryStream.ConfigureAwait(false))
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
+ foreach (var item in historyEntries)
{
- _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 insertScript = historyRepository.GetInsertScript(item);
+ await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
+ }
+
+ 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 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");
+
+ foreach (var entityType in entityTypes)
+ {
+ _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
+
+ 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);
@@ -242,22 +281,30 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false))
{
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
- var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
+ static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
+ {
+ var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
+ var enumerable = method.Invoke(dbSet, null)!;
+ return (IAsyncEnumerable<object>)enumerable;
+ }
+
+ // include the migration history as well
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
+
+ ICollection<(Type Type, 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, Set: e.GetValue(dbContext) as IQueryable))
- .ToArray();
+ .Select(e => (Type: e.PropertyType, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
+ (Type: typeof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => 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, Type type)
- {
- var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
- var enumerable = method.Invoke(dbSet, null)!;
- return (IAsyncEnumerable<object>)enumerable;
- }
foreach (var entityType in entityTypes)
{
@@ -272,7 +319,7 @@ public class BackupService : IBackupService
{
jsonSerializer.WriteStartArray();
- var set = GetValues(entityType.Set!, entityType.Type.PropertyType).ConfigureAwait(false);
+ var set = entityType.ValueFactory().ConfigureAwait(false);
await foreach (var item in set.ConfigureAwait(false))
{
entities++;
@@ -447,7 +494,8 @@ public class BackupService : IBackupService
{
Metadata = options.Metadata,
Subtitles = options.Subtitles,
- Trickplay = options.Trickplay
+ Trickplay = options.Trickplay,
+ Database = options.Database
};
}
@@ -457,7 +505,8 @@ public class BackupService : IBackupService
{
Metadata = options.Metadata,
Subtitles = options.Subtitles,
- Trickplay = options.Trickplay
+ Trickplay = options.Trickplay,
+ Database = options.Database
};
}
}