mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Add declarative backups for migrations (#14135)
This commit is contained in:
parent
0c46431cbb
commit
d5672ce407
@ -10,4 +10,6 @@ internal class BackupOptions
|
|||||||
public bool Trickplay { get; set; }
|
public bool Trickplay { get; set; }
|
||||||
|
|
||||||
public bool Subtitles { get; set; }
|
public bool Subtitles { get; set; }
|
||||||
|
|
||||||
|
public bool Database { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -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,6 +141,8 @@ public class BackupService : IBackupService
|
|||||||
CopyDirectory(_applicationPaths.DataPath, "Data/");
|
CopyDirectory(_applicationPaths.DataPath, "Data/");
|
||||||
CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
|
CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
|
||||||
|
|
||||||
|
if (manifest.Options.Database)
|
||||||
|
{
|
||||||
_logger.LogInformation("Begin restoring Database");
|
_logger.LogInformation("Begin restoring Database");
|
||||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
await using (dbContext.ConfigureAwait(false))
|
await using (dbContext.ConfigureAwait(false))
|
||||||
@ -218,6 +225,7 @@ public class BackupService : IBackupService
|
|||||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||||
_logger.LogInformation("Restored database.");
|
_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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,9 +47,9 @@ public sealed class JellyfinMigrationAttribute : Attribute
|
|||||||
public bool RunMigrationOnSetup { get; set; }
|
public bool RunMigrationOnSetup { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisaition"/>.
|
/// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisation"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition;
|
public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the ordering of the migration.
|
/// Gets the ordering of the migration.
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Server.Migrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks an <see cref="JellyfinMigrationAttribute"/> migration and instructs the <see cref="JellyfinMigrationService"/> to perform a backup.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
|
||||||
|
public sealed class JellyfinMigrationBackupAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets a value indicating whether a backup of the old library.db should be performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool LegacyLibraryDb { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets a value indicating whether a backup of the Database should be performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool JellyfinDb { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets a value indicating whether a backup of the metadata folder should be performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets a value indicating whether a backup of the Trickplay folder should be performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Trickplay { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or Sets a value indicating whether a backup of the Subtitles folder should be performed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Subtitles { get; set; }
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -25,21 +26,37 @@ namespace Jellyfin.Server.Migrations;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class JellyfinMigrationService
|
internal class JellyfinMigrationService
|
||||||
{
|
{
|
||||||
|
private const string DbFilename = "library.db";
|
||||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly IBackupService? _backupService;
|
||||||
|
private readonly IJellyfinDatabaseProvider? _jellyfinDatabaseProvider;
|
||||||
|
private readonly IApplicationPaths _applicationPaths;
|
||||||
|
private (string? LibraryDb, string? JellyfinDb, BackupManifestDto? FullBackup) _backupKey;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
|
/// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
|
/// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
|
||||||
/// <param name="loggerFactory">The logger factory.</param>
|
/// <param name="loggerFactory">The logger factory.</param>
|
||||||
public JellyfinMigrationService(IDbContextFactory<JellyfinDbContext> dbContextFactory, ILoggerFactory loggerFactory)
|
/// <param name="applicationPaths">Application paths for library.db backup.</param>
|
||||||
|
/// <param name="backupService">The jellyfin backup service.</param>
|
||||||
|
/// <param name="jellyfinDatabaseProvider">The jellyfin database provider.</param>
|
||||||
|
public JellyfinMigrationService(
|
||||||
|
IDbContextFactory<JellyfinDbContext> dbContextFactory,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IApplicationPaths applicationPaths,
|
||||||
|
IBackupService? backupService = null,
|
||||||
|
IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
|
_backupService = backupService;
|
||||||
|
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||||
|
_applicationPaths = applicationPaths;
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e))
|
Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e))
|
||||||
.Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>()))
|
.Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>(), Backup: e.GetCustomAttributes<JellyfinMigrationBackupAttribute>()))
|
||||||
.Where(e => e.Metadata != null)
|
.Where(e => e.Metadata != null)
|
||||||
.GroupBy(e => e.Metadata!.Stage)
|
.GroupBy(e => e.Metadata!.Stage)
|
||||||
.Select(f =>
|
.Select(f =>
|
||||||
@ -47,7 +64,13 @@ internal class JellyfinMigrationService
|
|||||||
var stage = new MigrationStage(f.Key);
|
var stage = new MigrationStage(f.Key);
|
||||||
foreach (var item in f)
|
foreach (var item in f)
|
||||||
{
|
{
|
||||||
stage.Add(new(item.Type, item.Metadata!));
|
JellyfinMigrationBackupAttribute? backupMetadata = null;
|
||||||
|
if (item.Backup?.Any() == true)
|
||||||
|
{
|
||||||
|
backupMetadata = item.Backup.Aggregate(MergeBackupAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
stage.Add(new(item.Type, item.Metadata!, backupMetadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
return stage;
|
return stage;
|
||||||
@ -155,7 +178,7 @@ internal class JellyfinMigrationService
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
|
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
|
||||||
if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
|
if (stage is JellyfinMigrationStageTypes.CoreInitialisation)
|
||||||
{
|
{
|
||||||
pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
||||||
.Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
|
.Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
|
||||||
@ -176,7 +199,51 @@ internal class JellyfinMigrationService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Migration {Name} failed", item.Key);
|
logger.LogCritical(ex, "Migration {Name} failed, migration service will attempt to roll back.", item.Key);
|
||||||
|
|
||||||
|
if (_backupKey != default && _backupService is not null && _jellyfinDatabaseProvider is not null)
|
||||||
|
{
|
||||||
|
if (_backupKey.LibraryDb is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to rollback librarydb.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
|
||||||
|
File.Move(_backupKey.LibraryDb, libraryDbPath, true);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.LibraryDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_backupKey.JellyfinDb is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to rollback JellyfinDb.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jellyfinDatabaseProvider.RestoreBackupFast(_backupKey.JellyfinDb, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not rollback {LibraryPath}. Manual intervention might be required to restore a operational state.", _backupKey.JellyfinDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_backupKey.FullBackup is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to rollback from backup.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _backupService.RestoreBackupAsync(_backupKey.FullBackup.Path).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not rollback from backup {Backup}. Manual intervention might be required to restore a operational state.", _backupKey.FullBackup.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,6 +255,143 @@ internal class JellyfinMigrationService
|
|||||||
return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
|
return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CleanupSystemAfterMigration(ILogger logger)
|
||||||
|
{
|
||||||
|
if (_backupKey != default)
|
||||||
|
{
|
||||||
|
if (_backupKey.LibraryDb is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to cleanup librarydb backup.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(_backupKey.LibraryDb);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.LibraryDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_backupKey.JellyfinDb is not null && _jellyfinDatabaseProvider is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to cleanup JellyfinDb backup.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jellyfinDatabaseProvider.DeleteBackup(_backupKey.JellyfinDb).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not cleanup {LibraryPath}.", _backupKey.JellyfinDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_backupKey.FullBackup is not null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Attempt to cleanup from migration backup.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(_backupKey.FullBackup.Path);
|
||||||
|
}
|
||||||
|
catch (Exception inner)
|
||||||
|
{
|
||||||
|
logger.LogCritical(inner, "Could not cleanup backup {Backup}.", _backupKey.FullBackup.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PrepareSystemForMigration(ILogger logger)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Prepare system for possible migrations");
|
||||||
|
JellyfinMigrationBackupAttribute backupInstruction;
|
||||||
|
IReadOnlyList<HistoryRow> appliedMigrations;
|
||||||
|
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||||
|
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
|
||||||
|
appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||||
|
backupInstruction = new JellyfinMigrationBackupAttribute()
|
||||||
|
{
|
||||||
|
JellyfinDb = migrationsAssembly.Migrations.Any(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
backupInstruction = Migrations.SelectMany(e => e)
|
||||||
|
.Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
|
||||||
|
.Select(e => e.BackupRequirements)
|
||||||
|
.Where(e => e is not null)
|
||||||
|
.Aggregate(backupInstruction, MergeBackupAttributes!);
|
||||||
|
|
||||||
|
if (backupInstruction.LegacyLibraryDb)
|
||||||
|
{
|
||||||
|
logger.LogInformation("A migration will attempt to modify the library.db, will attempt to backup the file now.");
|
||||||
|
// for legacy migrations that still operates on the library.db
|
||||||
|
var libraryDbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
|
||||||
|
if (File.Exists(libraryDbPath))
|
||||||
|
{
|
||||||
|
for (int i = 1; ; i++)
|
||||||
|
{
|
||||||
|
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", libraryDbPath, i);
|
||||||
|
if (!File.Exists(bakPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
|
||||||
|
File.Copy(libraryDbPath, bakPath);
|
||||||
|
_backupKey = (bakPath, _backupKey.JellyfinDb, _backupKey.FullBackup);
|
||||||
|
logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("{Library} has been backed up as {BackupPath}", DbFilename, _backupKey.LibraryDb);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogError("Cannot make a backup of {Library} at path {BackupPath} because file could not be found at {LibraryPath}", DbFilename, libraryDbPath, _applicationPaths.DataPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupInstruction.JellyfinDb && _jellyfinDatabaseProvider != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("A migration will attempt to modify the jellyfin.db, will attempt to backup the file now.");
|
||||||
|
_backupKey = (_backupKey.LibraryDb, await _jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false), _backupKey.FullBackup);
|
||||||
|
logger.LogInformation("Jellyfin database has been backed up as {BackupPath}", _backupKey.JellyfinDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_backupService is not null && (backupInstruction.Metadata || backupInstruction.Subtitles || backupInstruction.Trickplay))
|
||||||
|
{
|
||||||
|
logger.LogInformation("A migration will attempt to modify system resources. Will attempt to create backup now.");
|
||||||
|
_backupKey = (_backupKey.LibraryDb, _backupKey.JellyfinDb, await _backupService.CreateBackupAsync(new BackupOptionsDto()
|
||||||
|
{
|
||||||
|
Metadata = backupInstruction.Metadata,
|
||||||
|
Subtitles = backupInstruction.Subtitles,
|
||||||
|
Trickplay = backupInstruction.Trickplay,
|
||||||
|
Database = false // database backups are explicitly handled by the provider itself as the backup service requires parity with the current model
|
||||||
|
}).ConfigureAwait(false));
|
||||||
|
logger.LogInformation("Pre-Migration backup successfully created as {BackupKey}", _backupKey.FullBackup.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JellyfinMigrationBackupAttribute MergeBackupAttributes(JellyfinMigrationBackupAttribute left, JellyfinMigrationBackupAttribute right)
|
||||||
|
{
|
||||||
|
return new JellyfinMigrationBackupAttribute()
|
||||||
|
{
|
||||||
|
JellyfinDb = left!.JellyfinDb || right!.JellyfinDb,
|
||||||
|
LegacyLibraryDb = left.LegacyLibraryDb || right!.LegacyLibraryDb,
|
||||||
|
Metadata = left.Metadata || right!.Metadata,
|
||||||
|
Subtitles = left.Subtitles || right!.Subtitles,
|
||||||
|
Trickplay = left.Trickplay || right!.Trickplay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private class InternalCodeMigration : IInternalMigration
|
private class InternalCodeMigration : IInternalMigration
|
||||||
{
|
{
|
||||||
private readonly CodeMigration _codeMigration;
|
private readonly CodeMigration _codeMigration;
|
||||||
|
@ -18,10 +18,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
[JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
|
[JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
|
||||||
|
[JellyfinMigrationBackup(LegacyLibraryDb = true)]
|
||||||
internal class FixAudioData : IMigrationRoutine
|
internal class FixAudioData : IMigrationRoutine
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
{
|
{
|
||||||
private const string DbFilename = "library.db";
|
|
||||||
private readonly ILogger<FixAudioData> _logger;
|
private readonly ILogger<FixAudioData> _logger;
|
||||||
private readonly IServerApplicationPaths _applicationPaths;
|
private readonly IServerApplicationPaths _applicationPaths;
|
||||||
private readonly IItemRepository _itemRepository;
|
private readonly IItemRepository _itemRepository;
|
||||||
@ -39,29 +39,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Perform()
|
public void Perform()
|
||||||
{
|
{
|
||||||
var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
|
|
||||||
|
|
||||||
// Back up the database before modifying any entries
|
|
||||||
for (int i = 1; ; i++)
|
|
||||||
{
|
|
||||||
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
|
|
||||||
if (!File.Exists(bakPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
|
|
||||||
File.Copy(dbPath, bakPath);
|
|
||||||
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Backfilling audio lyrics data to database.");
|
_logger.LogInformation("Backfilling audio lyrics data to database.");
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
var records = _itemRepository.GetCount(new InternalItemsQuery
|
var records = _itemRepository.GetCount(new InternalItemsQuery
|
||||||
|
@ -29,6 +29,7 @@ namespace Jellyfin.Server.Migrations.Routines;
|
|||||||
/// The migration routine for migrating the userdata database to EF Core.
|
/// The migration routine for migrating the userdata database to EF Core.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
|
[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb))]
|
||||||
|
[JellyfinMigrationBackup(JellyfinDb = true, LegacyLibraryDb = true)]
|
||||||
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||||
{
|
{
|
||||||
private const string DbFilename = "library.db";
|
private const string DbFilename = "library.db";
|
||||||
|
@ -12,6 +12,7 @@ namespace Jellyfin.Server.Migrations.Routines;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
|
[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels))]
|
||||||
|
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
|
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
|
||||||
{
|
{
|
||||||
|
@ -6,12 +6,14 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Stages;
|
namespace Jellyfin.Server.Migrations.Stages;
|
||||||
|
|
||||||
internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata)
|
internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata, JellyfinMigrationBackupAttribute? migrationBackupAttribute)
|
||||||
{
|
{
|
||||||
public Type MigrationType { get; } = migrationType;
|
public Type MigrationType { get; } = migrationType;
|
||||||
|
|
||||||
public JellyfinMigrationAttribute Metadata { get; } = metadata;
|
public JellyfinMigrationAttribute Metadata { get; } = metadata;
|
||||||
|
|
||||||
|
public JellyfinMigrationBackupAttribute? BackupRequirements { get; set; } = migrationBackupAttribute;
|
||||||
|
|
||||||
public string BuildCodeMigrationId()
|
public string BuildCodeMigrationId()
|
||||||
{
|
{
|
||||||
return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!;
|
return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!;
|
||||||
|
@ -17,7 +17,7 @@ public enum JellyfinMigrationStageTypes
|
|||||||
/// Runs after the host has been configured and includes the database migrations.
|
/// Runs after the host has been configured and includes the database migrations.
|
||||||
/// Allows the mix order of migrations that contain application code and database changes.
|
/// Allows the mix order of migrations that contain application code and database changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CoreInitialisaition = 2,
|
CoreInitialisation = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs after services has been registered and initialised. Last step before running the server.
|
/// Runs after services has been registered and initialised. Last step before running the server.
|
||||||
|
@ -16,10 +16,10 @@ using Jellyfin.Server.Extensions;
|
|||||||
using Jellyfin.Server.Helpers;
|
using Jellyfin.Server.Helpers;
|
||||||
using Jellyfin.Server.Implementations.DatabaseConfiguration;
|
using Jellyfin.Server.Implementations.DatabaseConfiguration;
|
||||||
using Jellyfin.Server.Implementations.Extensions;
|
using Jellyfin.Server.Implementations.Extensions;
|
||||||
using Jellyfin.Server.Implementations.FullSystemBackup;
|
|
||||||
using Jellyfin.Server.Implementations.StorageHelpers;
|
using Jellyfin.Server.Implementations.StorageHelpers;
|
||||||
using Jellyfin.Server.Implementations.SystemBackupService;
|
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||||
using Jellyfin.Server.Migrations;
|
using Jellyfin.Server.Migrations;
|
||||||
|
using Jellyfin.Server.Migrations.Stages;
|
||||||
using Jellyfin.Server.ServerSetupApp;
|
using Jellyfin.Server.ServerSetupApp;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
@ -190,12 +190,14 @@ namespace Jellyfin.Server
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false);
|
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(appHost.ServiceProvider);
|
||||||
|
await jellyfinMigrationService.PrepareSystemForMigration(_logger).ConfigureAwait(false);
|
||||||
|
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
|
||||||
|
|
||||||
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
||||||
|
|
||||||
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).ConfigureAwait(false);
|
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.AppInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
|
||||||
|
await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _setupServer!.StopAsync().ConfigureAwait(false);
|
await _setupServer!.StopAsync().ConfigureAwait(false);
|
||||||
|
@ -21,4 +21,9 @@ public class BackupOptionsDto
|
|||||||
/// Gets or sets a value indicating whether the archive contains the Subtitle contents.
|
/// Gets or sets a value indicating whether the archive contains the Subtitle contents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Subtitles { get; set; }
|
public bool Subtitles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the archive contains the Database contents.
|
||||||
|
/// </summary>
|
||||||
|
public bool Database { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,13 @@ public interface IJellyfinDatabaseProvider
|
|||||||
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||||
Task RestoreBackupFast(string key, CancellationToken cancellationToken);
|
Task RestoreBackupFast(string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a backup that has been previously created by <see cref="MigrationBackupFast(CancellationToken)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to the backup which should be cleaned up.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||||
|
Task DeleteBackup(string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all contents from the database.
|
/// Removes all contents from the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -129,6 +129,21 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task DeleteBackup(string key)
|
||||||
|
{
|
||||||
|
var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
|
||||||
|
|
||||||
|
if (!File.Exists(backupFile))
|
||||||
|
{
|
||||||
|
_logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Delete(backupFile);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
|
public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
|
||||||
{
|
{
|
||||||
|
@ -106,7 +106,7 @@ namespace Jellyfin.Server.Integration.Tests
|
|||||||
appHost.ServiceProvider = host.Services;
|
appHost.ServiceProvider = host.Services;
|
||||||
var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>();
|
var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>();
|
||||||
Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult();
|
Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult();
|
||||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).GetAwaiter().GetResult();
|
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisation).GetAwaiter().GetResult();
|
||||||
appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult();
|
appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult();
|
||||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
|
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
|
||||||
host.Start();
|
host.Start();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user