aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
authorJPVenson <github@jpb.email>2025-05-19 03:39:04 +0300
committerGitHub <noreply@github.com>2025-05-18 18:39:04 -0600
commitfe2596dc0e389c0496a384cc1893fddd4742ed37 (patch)
tree8d215ed776cbf399ea5545a737858690e79f4cb1 /Jellyfin.Server
parentcdbf4752b9834506a6782db357f95924902081a8 (diff)
Add Full system backup feature (#13945)
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs43
-rw-r--r--Jellyfin.Server/Program.cs14
-rw-r--r--Jellyfin.Server/StartupOptions.cs6
3 files changed, 46 insertions, 17 deletions
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index ebffab7ef..3d6ed73bc 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -7,8 +7,10 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Migrations.Stages;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.SystemBackupService;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -103,25 +105,33 @@ internal class JellyfinMigrationService
if (migrationOptions != null && migrationOptions.Applied.Count > 0)
{
logger.LogInformation("Old migration style migration.xml detected. Migrate now.");
- var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
+ try
{
- var historyRepository = dbContext.GetService<IHistoryRepository>();
- var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
- var oldMigrations = Migrations
- .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
- .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
- .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
- .ToArray();
- var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
- foreach (var item in startupScripts)
+ var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
- await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+ var historyRepository = dbContext.GetService<IHistoryRepository>();
+ var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
+ var oldMigrations = Migrations
+ .SelectMany(e => e.Where(e => e.Metadata.Key is not null)) // only consider migrations that have the key set as its the reference marker for legacy migrations.
+ .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value)))
+ .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId()))
+ .ToArray();
+ var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))));
+ foreach (var item in startupScripts)
+ {
+ logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
+ await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
+ }
+
+ logger.LogInformation("Rename old migration.xml to migration.xml.backup");
+ File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
}
-
- logger.LogInformation("Rename old migration.xml to migration.xml.backup");
- File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true);
+ }
+ catch (Exception ex)
+ {
+ logger.LogCritical(ex, "Failed to apply migrations");
+ throw;
}
}
}
@@ -155,6 +165,7 @@ internal class JellyfinMigrationService
(string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations];
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+
foreach (var item in migrations)
{
try
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 745f92420..4584b25bd 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -16,7 +16,9 @@ using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations.DatabaseConfiguration;
using Jellyfin.Server.Implementations.Extensions;
+using Jellyfin.Server.Implementations.FullSystemBackup;
using Jellyfin.Server.Implementations.StorageHelpers;
+using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Migrations;
using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
@@ -58,6 +60,7 @@ namespace Jellyfin.Server
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
+ private static string? _restoreFromBackup;
/// <summary>
/// The entry point of the application.
@@ -79,6 +82,7 @@ namespace Jellyfin.Server
private static async Task StartApp(StartupOptions options)
{
+ _restoreFromBackup = options.RestoreArchive;
_startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
appPaths.MakeSanityCheckOrThrow();
@@ -176,9 +180,16 @@ namespace Jellyfin.Server
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
appHost.ServiceProvider = _jellyfinHost.Services;
-
PrepareDatabaseProvider(appHost.ServiceProvider);
+ if (!string.IsNullOrWhiteSpace(_restoreFromBackup))
+ {
+ await appHost.ServiceProvider.GetService<IBackupService>()!.RestoreBackupAsync(_restoreFromBackup).ConfigureAwait(false);
+ _restoreFromBackup = null;
+ _restartOnShutdown = true;
+ return;
+ }
+
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false);
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
@@ -209,6 +220,7 @@ namespace Jellyfin.Server
await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
+ _restoreFromBackup = appHost.RestoreBackupPath;
}
catch (Exception ex)
{
diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs
index 91ac827ca..4890ccbb2 100644
--- a/Jellyfin.Server/StartupOptions.cs
+++ b/Jellyfin.Server/StartupOptions.cs
@@ -74,6 +74,12 @@ namespace Jellyfin.Server
public bool NoDetectNetworkChange { get; set; }
/// <summary>
+ /// Gets or sets the path to an jellyfin backup archive to restore the application to.
+ /// </summary>
+ [Option("restore-archive", Required = false, HelpText = "Path to a Jellyfin backup archive to restore from")]
+ public string? RestoreArchive { get; set; }
+
+ /// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
/// </summary>
/// <returns>The configuration dictionary.</returns>