Backup MigrationHistory as well (#14136)

This commit is contained in:
JPVenson 2025-06-04 00:15:46 +03:00 committed by GitHub
parent a1d72deba2
commit 697bb6a480
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -14,6 +14,8 @@ using Jellyfin.Server.Implementations.SystemBackupService;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.SystemBackupService; using MediaBrowser.Controller.SystemBackupService;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.FullSystemBackup; namespace Jellyfin.Server.Implementations.FullSystemBackup;
@ -133,6 +135,30 @@ public class BackupService : IBackupService
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.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.");
}
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.");
}
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);
}
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
@ -242,22 +268,30 @@ public class BackupService : IBackupService
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 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))) .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
.Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable)) .Select(e => (Type: e.PropertyType, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
.ToArray(); (Type: typeof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray(); manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false); var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false)) await using (transaction.ConfigureAwait(false))
{ {
_logger.LogInformation("Begin Database backup"); _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) foreach (var entityType in entityTypes)
{ {
@ -272,7 +306,7 @@ public class BackupService : IBackupService
{ {
jsonSerializer.WriteStartArray(); 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)) await foreach (var item in set.ConfigureAwait(false))
{ {
entities++; entities++;