|
@@ -16,6 +16,7 @@ using MediaBrowser.Controller.SystemBackupService;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
|
|
+using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
namespace Jellyfin.Server.Implementations.FullSystemBackup;
|
|
namespace Jellyfin.Server.Implementations.FullSystemBackup;
|
|
@@ -31,7 +32,7 @@ public class BackupService : IBackupService
|
|
private readonly IServerApplicationHost _applicationHost;
|
|
private readonly IServerApplicationHost _applicationHost;
|
|
private readonly IServerApplicationPaths _applicationPaths;
|
|
private readonly IServerApplicationPaths _applicationPaths;
|
|
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
|
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
|
- private readonly ISystemManager _systemManager;
|
|
|
|
|
|
+ private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
|
private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
|
|
private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
|
|
{
|
|
{
|
|
AllowTrailingCommas = true,
|
|
AllowTrailingCommas = true,
|
|
@@ -48,21 +49,21 @@ public class BackupService : IBackupService
|
|
/// <param name="applicationHost">The Application host.</param>
|
|
/// <param name="applicationHost">The Application host.</param>
|
|
/// <param name="applicationPaths">The application paths.</param>
|
|
/// <param name="applicationPaths">The application paths.</param>
|
|
/// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</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(
|
|
public BackupService(
|
|
ILogger<BackupService> logger,
|
|
ILogger<BackupService> logger,
|
|
IDbContextFactory<JellyfinDbContext> dbProvider,
|
|
IDbContextFactory<JellyfinDbContext> dbProvider,
|
|
IServerApplicationHost applicationHost,
|
|
IServerApplicationHost applicationHost,
|
|
IServerApplicationPaths applicationPaths,
|
|
IServerApplicationPaths applicationPaths,
|
|
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
|
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
|
- ISystemManager systemManager)
|
|
|
|
|
|
+ IHostApplicationLifetime applicationLifetime)
|
|
{
|
|
{
|
|
_logger = logger;
|
|
_logger = logger;
|
|
_dbProvider = dbProvider;
|
|
_dbProvider = dbProvider;
|
|
_applicationHost = applicationHost;
|
|
_applicationHost = applicationHost;
|
|
_applicationPaths = applicationPaths;
|
|
_applicationPaths = applicationPaths;
|
|
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
|
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
|
- _systemManager = systemManager;
|
|
|
|
|
|
+ _hostApplicationLifetime = applicationLifetime;
|
|
}
|
|
}
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
@@ -71,7 +72,11 @@ public class BackupService : IBackupService
|
|
_applicationHost.RestoreBackupPath = archivePath;
|
|
_applicationHost.RestoreBackupPath = archivePath;
|
|
_applicationHost.ShouldRestart = true;
|
|
_applicationHost.ShouldRestart = true;
|
|
_applicationHost.NotifyPendingRestart();
|
|
_applicationHost.NotifyPendingRestart();
|
|
- _systemManager.Restart();
|
|
|
|
|
|
+ _ = Task.Run(async () =>
|
|
|
|
+ {
|
|
|
|
+ await Task.Delay(500).ConfigureAwait(false);
|
|
|
|
+ _hostApplicationLifetime.StopApplication();
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
@@ -136,87 +141,90 @@ public class BackupService : IBackupService
|
|
CopyDirectory(_applicationPaths.DataPath, "Data/");
|
|
CopyDirectory(_applicationPaths.DataPath, "Data/");
|
|
CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
|
|
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("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))
|
|
|
|
|
|
+ _logger.LogInformation("Begin restoring Database");
|
|
|
|
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
|
|
|
+ await using (dbContext.ConfigureAwait(false))
|
|
{
|
|
{
|
|
- historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).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.");
|
|
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);
|
|
|
|
- }
|
|
|
|
|
|
+ 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.");
|
|
|
|
+ }
|
|
|
|
|
|
- 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 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);
|
|
|
|
+ }
|
|
|
|
|
|
- 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");
|
|
|
|
|
|
+ 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();
|
|
|
|
|
|
- foreach (var entityType in entityTypes)
|
|
|
|
- {
|
|
|
|
- _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
|
|
|
|
|
|
+ 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");
|
|
|
|
|
|
- var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
|
|
|
|
- if (zipEntry is null)
|
|
|
|
|
|
+ foreach (var entityType in entityTypes)
|
|
{
|
|
{
|
|
- _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
|
|
|
|
- continue;
|
|
|
|
- }
|
|
|
|
|
|
+ _logger.LogInformation("Read backup of {Table}", 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 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);
|
|
_logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
|
|
@@ -486,7 +494,8 @@ public class BackupService : IBackupService
|
|
{
|
|
{
|
|
Metadata = options.Metadata,
|
|
Metadata = options.Metadata,
|
|
Subtitles = options.Subtitles,
|
|
Subtitles = options.Subtitles,
|
|
- Trickplay = options.Trickplay
|
|
|
|
|
|
+ Trickplay = options.Trickplay,
|
|
|
|
+ Database = options.Database
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
@@ -496,7 +505,8 @@ public class BackupService : IBackupService
|
|
{
|
|
{
|
|
Metadata = options.Metadata,
|
|
Metadata = options.Metadata,
|
|
Subtitles = options.Subtitles,
|
|
Subtitles = options.Subtitles,
|
|
- Trickplay = options.Trickplay
|
|
|
|
|
|
+ Trickplay = options.Trickplay,
|
|
|
|
+ Database = options.Database
|
|
};
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|