mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Add declarative backups for migrations (#14135)
This commit is contained in:
@@ -16,6 +16,7 @@ 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;
|
||||
@@ -31,7 +32,7 @@ public class BackupService : IBackupService
|
||||
private readonly IServerApplicationHost _applicationHost;
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
||||
private readonly ISystemManager _systemManager;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
@@ -48,21 +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="systemManager">The SystemManager.</param>
|
||||
/// <param name="applicationLifetime">The SystemManager.</param>
|
||||
public BackupService(
|
||||
ILogger<BackupService> logger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IServerApplicationHost applicationHost,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
||||
ISystemManager systemManager)
|
||||
IHostApplicationLifetime applicationLifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
_applicationHost = applicationHost;
|
||||
_applicationPaths = applicationPaths;
|
||||
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||
_systemManager = systemManager;
|
||||
_hostApplicationLifetime = applicationLifetime;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -71,7 +72,11 @@ public class BackupService : IBackupService
|
||||
_applicationHost.RestoreBackupPath = archivePath;
|
||||
_applicationHost.ShouldRestart = true;
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
_systemManager.Restart();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(500).ConfigureAwait(false);
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -136,87 +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)
|
||||
{
|
||||
// restore migration history manually
|
||||
var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
|
||||
if (historyEntry is null)
|
||||
_logger.LogInformation("Begin restoring Database");
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
_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;
|
||||
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)
|
||||
// restore migration history manually
|
||||
var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
|
||||
if (historyEntry is null)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
continue;
|
||||
_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 zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
HistoryRow[] historyEntries;
|
||||
var historyArchive = historyEntry.Open();
|
||||
await using (historyArchive.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)!)
|
||||
{
|
||||
var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
|
||||
if (entity is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
|
||||
}
|
||||
historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ??
|
||||
throw new InvalidOperationException("Cannot restore backup that has no History data.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
records++;
|
||||
dbContext.Add(entity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
|
||||
}
|
||||
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;
|
||||
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)
|
||||
{
|
||||
_logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityType.Type.Name);
|
||||
}
|
||||
}
|
||||
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)!)
|
||||
{
|
||||
var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
|
||||
if (entity is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Try restore Database");
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("Restored database.");
|
||||
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("Try restore Database");
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("Restored database.");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
|
||||
@@ -486,7 +494,8 @@ public class BackupService : IBackupService
|
||||
{
|
||||
Metadata = options.Metadata,
|
||||
Subtitles = options.Subtitles,
|
||||
Trickplay = options.Trickplay
|
||||
Trickplay = options.Trickplay,
|
||||
Database = options.Database
|
||||
};
|
||||
}
|
||||
|
||||
@@ -496,7 +505,8 @@ public class BackupService : IBackupService
|
||||
{
|
||||
Metadata = options.Metadata,
|
||||
Subtitles = options.Subtitles,
|
||||
Trickplay = options.Trickplay
|
||||
Trickplay = options.Trickplay,
|
||||
Database = options.Database
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user