mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Unified migration handling (#13950)
This commit is contained in:
parent
1c4b5199b8
commit
e66c76fc34
@ -580,21 +580,6 @@ namespace Emby.Server.Implementations
|
||||
/// <returns>A task representing the service initialization operation.</returns>
|
||||
public async Task InitializeServices(IConfiguration startupConfig)
|
||||
{
|
||||
var factory = Resolve<IDbContextFactory<JellyfinDbContext>>();
|
||||
var provider = Resolve<IJellyfinDatabaseProvider>();
|
||||
provider.DbContextFactory = factory;
|
||||
|
||||
var jellyfinDb = await factory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (jellyfinDb.ConfigureAwait(false))
|
||||
{
|
||||
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
|
||||
{
|
||||
Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
|
||||
await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
|
||||
Logger.LogInformation("EFCore migrations applied successfully");
|
||||
}
|
||||
}
|
||||
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
|
31
Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs
Normal file
31
Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin.Server.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Interface that describes a migration routine.
|
||||
/// </summary>
|
||||
internal interface IAsyncMigrationRoutine
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute the migration routine.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token triggered if the migration should be aborted.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public Task PerformAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface that describes a migration routine.
|
||||
/// </summary>
|
||||
[Obsolete("Use IAsyncMigrationRoutine instead")]
|
||||
internal interface IMigrationRoutine
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute the migration routine.
|
||||
/// </summary>
|
||||
[Obsolete("Use IAsyncMigrationRoutine.PerformAsync instead")]
|
||||
public void Perform();
|
||||
}
|
@ -7,6 +7,8 @@ namespace Jellyfin.Server.Migrations;
|
||||
/// <summary>
|
||||
/// Defines a migration that operates on the Database.
|
||||
/// </summary>
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal interface IDatabaseMigrationRoutine : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
|
||||
namespace Jellyfin.Server.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that describes a migration routine.
|
||||
/// </summary>
|
||||
internal interface IMigrationRoutine
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique id for this migration. This should never be modified after the migration has been created.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name of the migration.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to perform migration on a new install.
|
||||
/// </summary>
|
||||
public bool PerformOnNewInstall { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Execute the migration routine.
|
||||
/// </summary>
|
||||
public void Perform();
|
||||
}
|
||||
}
|
65
Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
Normal file
65
Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs
Normal file
@ -0,0 +1,65 @@
|
||||
#pragma warning disable CA1019 // Define accessors for attribute arguments
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Server.Migrations.Stages;
|
||||
|
||||
namespace Jellyfin.Server.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Declares an class as an migration with its set metadata.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class JellyfinMigrationAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellyfinMigrationAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
|
||||
/// <param name="name">The name of this Migration.</param>
|
||||
public JellyfinMigrationAttribute(string order, string name) : this(order, name, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellyfinMigrationAttribute"/> class for legacy migrations.
|
||||
/// </summary>
|
||||
/// <param name="order">The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string.</param>
|
||||
/// <param name="name">The name of this Migration.</param>
|
||||
/// <param name="key">[ONLY FOR LEGACY MIGRATIONS]The unique key of this migration. Must be a valid Guid formatted string.</param>
|
||||
public JellyfinMigrationAttribute(string order, string name, string? key)
|
||||
{
|
||||
Order = DateTime.Parse(order, CultureInfo.InvariantCulture);
|
||||
Name = name;
|
||||
Stage = JellyfinMigrationStageTypes.AppInitialisation;
|
||||
if (key is not null)
|
||||
{
|
||||
Key = Guid.Parse(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets a value indicating whether the annoated migration should be executed on a fresh install.
|
||||
/// </summary>
|
||||
public bool RunMigrationOnSetup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the stage the annoated migration should be executed at. Defaults to <see cref="JellyfinMigrationStageTypes.CoreInitialisaition"/>.
|
||||
/// </summary>
|
||||
public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordering of the migration.
|
||||
/// </summary>
|
||||
public DateTime Order { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the migration.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Legacy Key of the migration. Not required for new Migrations.
|
||||
/// </summary>
|
||||
public Guid? Key { get; }
|
||||
}
|
219
Jellyfin.Server/Migrations/JellyfinMigrationService.cs
Normal file
219
Jellyfin.Server/Migrations/JellyfinMigrationService.cs
Normal file
@ -0,0 +1,219 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Migrations.Stages;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations;
|
||||
|
||||
/// <summary>
|
||||
/// Handles Migration of the Jellyfin data structure.
|
||||
/// </summary>
|
||||
internal class JellyfinMigrationService
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JellyfinMigrationService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">Provides access to the jellyfin database.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public JellyfinMigrationService(IDbContextFactory<JellyfinDbContext> dbContextFactory, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_loggerFactory = loggerFactory;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e))
|
||||
.Select(e => (Type: e, Metadata: e.GetCustomAttribute<JellyfinMigrationAttribute>()))
|
||||
.Where(e => e.Metadata != null)
|
||||
.GroupBy(e => e.Metadata!.Stage)
|
||||
.Select(f =>
|
||||
{
|
||||
var stage = new MigrationStage(f.Key);
|
||||
foreach (var item in f)
|
||||
{
|
||||
stage.Add(new(item.Type, item.Metadata!));
|
||||
}
|
||||
|
||||
return stage;
|
||||
})];
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
private interface IInternalMigration
|
||||
{
|
||||
Task PerformAsync(ILogger logger);
|
||||
}
|
||||
|
||||
private HashSet<MigrationStage> Migrations { get; set; }
|
||||
|
||||
public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths)
|
||||
{
|
||||
var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
|
||||
logger.LogInformation("Initialise Migration service.");
|
||||
var xmlSerializer = new MyXmlSerializer();
|
||||
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
||||
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
||||
: new ServerConfiguration();
|
||||
if (!serverConfig.IsStartupWizardCompleted)
|
||||
{
|
||||
logger.LogInformation("System initialisation detected. Seed data.");
|
||||
var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray();
|
||||
|
||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
|
||||
await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
|
||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
var startupScripts = flatApplyMigrations
|
||||
.Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId()))
|
||||
.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion()))))
|
||||
.ToArray();
|
||||
foreach (var item in startupScripts)
|
||||
{
|
||||
logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name);
|
||||
await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Migration system initialisation completed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// migrate any existing migration.xml files
|
||||
var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml");
|
||||
var migrationOptions = File.Exists(migrationConfigPath)
|
||||
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
||||
: null;
|
||||
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))
|
||||
{
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
var oldMigrations = Migrations.SelectMany(e => e)
|
||||
.Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value))) // this is a legacy migration that will always have its own ID.
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider)
|
||||
{
|
||||
var logger = _loggerFactory.CreateLogger<JellyfinMigrationService>();
|
||||
logger.LogInformation("Migrate stage {Stage}.", stage);
|
||||
ICollection<CodeMigration> migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection<CodeMigration>) ?? [];
|
||||
|
||||
var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
|
||||
var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
var pendingCodeMigrations = migrationStage
|
||||
.Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId()))
|
||||
.Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext)))
|
||||
.ToArray();
|
||||
|
||||
(string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = [];
|
||||
if (stage is JellyfinMigrationStageTypes.CoreInitialisaition)
|
||||
{
|
||||
pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key))
|
||||
.Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
(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
|
||||
{
|
||||
logger.LogInformation("Perform migration {Name}", item.Key);
|
||||
await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(false);
|
||||
logger.LogInformation("Migration {Name} was successfully applied", item.Key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, "Migration {Name} failed", item.Key);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetJellyfinVersion()
|
||||
{
|
||||
return Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
|
||||
}
|
||||
|
||||
private class InternalCodeMigration : IInternalMigration
|
||||
{
|
||||
private readonly CodeMigration _codeMigration;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
private JellyfinDbContext _dbContext;
|
||||
|
||||
public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext dbContext)
|
||||
{
|
||||
_codeMigration = codeMigration;
|
||||
_serviceProvider = serviceProvider;
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
public async Task PerformAsync(ILogger logger)
|
||||
{
|
||||
await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var historyRepository = _dbContext.GetService<IHistoryRepository>();
|
||||
var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion()));
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalDatabaseMigration : IInternalMigration
|
||||
{
|
||||
private readonly JellyfinDbContext _jellyfinDbContext;
|
||||
private KeyValuePair<string, TypeInfo> _databaseMigrationInfo;
|
||||
|
||||
public InternalDatabaseMigration(KeyValuePair<string, TypeInfo> databaseMigrationInfo, JellyfinDbContext jellyfinDbContext)
|
||||
{
|
||||
_databaseMigrationInfo = databaseMigrationInfo;
|
||||
_jellyfinDbContext = jellyfinDbContext;
|
||||
}
|
||||
|
||||
public async Task PerformAsync(ILogger logger)
|
||||
{
|
||||
var migrator = _jellyfinDbContext.GetService<IMigrator>();
|
||||
await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// The class that knows which migrations to apply and how to apply them.
|
||||
/// </summary>
|
||||
public sealed class MigrationRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of known pre-startup migrations, in order of applicability.
|
||||
/// </summary>
|
||||
private static readonly Type[] _preStartupMigrationTypes =
|
||||
{
|
||||
typeof(PreStartupRoutines.CreateNetworkConfiguration),
|
||||
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
|
||||
typeof(PreStartupRoutines.MigrateNetworkConfiguration),
|
||||
typeof(PreStartupRoutines.MigrateEncodingOptions),
|
||||
typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The list of known migrations, in order of applicability.
|
||||
/// </summary>
|
||||
private static readonly Type[] _migrationTypes =
|
||||
{
|
||||
typeof(Routines.DisableTranscodingThrottling),
|
||||
typeof(Routines.CreateUserLoggingConfigFile),
|
||||
typeof(Routines.MigrateActivityLogDb),
|
||||
typeof(Routines.RemoveDuplicateExtras),
|
||||
typeof(Routines.AddDefaultPluginRepository),
|
||||
typeof(Routines.MigrateUserDb),
|
||||
typeof(Routines.ReaddDefaultPluginRepository),
|
||||
typeof(Routines.MigrateDisplayPreferencesDb),
|
||||
typeof(Routines.RemoveDownloadImagesInAdvance),
|
||||
typeof(Routines.MigrateAuthenticationDb),
|
||||
typeof(Routines.FixPlaylistOwner),
|
||||
typeof(Routines.AddDefaultCastReceivers),
|
||||
typeof(Routines.UpdateDefaultPluginRepository),
|
||||
typeof(Routines.FixAudioData),
|
||||
typeof(Routines.RemoveDuplicatePlaylistChildren),
|
||||
typeof(Routines.MigrateLibraryDb),
|
||||
typeof(Routines.MoveExtractedFiles),
|
||||
typeof(Routines.MigrateRatingLevels),
|
||||
typeof(Routines.MoveTrickplayFiles),
|
||||
typeof(Routines.MigrateKeyframeData),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Run all needed migrations.
|
||||
/// </summary>
|
||||
/// <param name="host">CoreAppHost that hosts current version.</param>
|
||||
/// <param name="loggerFactory">Factory for making the logger.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger<MigrationRunner>();
|
||||
var migrations = _migrationTypes
|
||||
.Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
|
||||
.OfType<IMigrationRoutine>()
|
||||
.ToArray();
|
||||
|
||||
var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
|
||||
HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger);
|
||||
await PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger, host.ServiceProvider.GetRequiredService<IJellyfinDatabaseProvider>())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run all needed pre-startup migrations.
|
||||
/// </summary>
|
||||
/// <param name="appPaths">Application paths.</param>
|
||||
/// <param name="loggerFactory">Factory for making the logger.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger<MigrationRunner>();
|
||||
var migrations = _preStartupMigrationTypes
|
||||
.Select(m => Activator.CreateInstance(m, appPaths, loggerFactory))
|
||||
.OfType<IMigrationRoutine>()
|
||||
.ToArray();
|
||||
|
||||
var xmlSerializer = new MyXmlSerializer();
|
||||
var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml");
|
||||
var migrationOptions = File.Exists(migrationConfigPath)
|
||||
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
|
||||
: new MigrationOptions();
|
||||
|
||||
// We have to deserialize it manually since the configuration manager may overwrite it
|
||||
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
|
||||
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
|
||||
: new ServerConfiguration();
|
||||
|
||||
HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger);
|
||||
await PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger)
|
||||
{
|
||||
if (isStartWizardCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If startup wizard is not finished, this is a fresh install.
|
||||
var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray();
|
||||
logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name));
|
||||
migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name)));
|
||||
}
|
||||
|
||||
private static async Task PerformMigrations(
|
||||
IMigrationRoutine[] migrations,
|
||||
MigrationOptions migrationOptions,
|
||||
Action<MigrationOptions> saveConfiguration,
|
||||
ILogger logger,
|
||||
IJellyfinDatabaseProvider? jellyfinDatabaseProvider)
|
||||
{
|
||||
// save already applied migrations, and skip them thereafter
|
||||
saveConfiguration(migrationOptions);
|
||||
var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
|
||||
var migrationsToBeApplied = migrations.Where(e => !appliedMigrationIds.Contains(e.Id)).ToArray();
|
||||
|
||||
string? migrationKey = null;
|
||||
if (jellyfinDatabaseProvider is not null && migrationsToBeApplied.Any(f => f is IDatabaseMigrationRoutine))
|
||||
{
|
||||
logger.LogInformation("Performing database backup");
|
||||
try
|
||||
{
|
||||
migrationKey = await jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false);
|
||||
logger.LogInformation("Database backup with key '{BackupKey}' has been successfully created.", migrationKey);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
logger.LogWarning("Could not perform backup of database before migration because provider does not support it");
|
||||
}
|
||||
}
|
||||
|
||||
List<IMigrationRoutine> databaseMigrations = [];
|
||||
try
|
||||
{
|
||||
foreach (var migrationRoutine in migrationsToBeApplied)
|
||||
{
|
||||
logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name);
|
||||
var isDbMigration = migrationRoutine is IDatabaseMigrationRoutine;
|
||||
|
||||
if (isDbMigration)
|
||||
{
|
||||
databaseMigrations.Add(migrationRoutine);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
migrationRoutine.Perform();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Mark the migration as completed
|
||||
logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
|
||||
if (!isDbMigration)
|
||||
{
|
||||
migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
|
||||
saveConfiguration(migrationOptions);
|
||||
logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null)
|
||||
{
|
||||
if (databaseMigrations.Count != 0)
|
||||
{
|
||||
logger.LogInformation("Rolling back database as migrations reported failure.");
|
||||
await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
foreach (var migrationRoutine in databaseMigrations)
|
||||
{
|
||||
migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
|
||||
saveConfiguration(migrationOptions);
|
||||
logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Server.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// A factory that can find a persistent file of the migration configuration, which lists all applied migrations.
|
||||
/// </summary>
|
||||
public class MigrationsFactory : IConfigurationFactory
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new MigrationsListStore()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Server.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// A configuration that lists all the migration routines that were applied.
|
||||
/// </summary>
|
||||
public class MigrationsListStore : ConfigurationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the configuration in the storage.
|
||||
/// </summary>
|
||||
public static readonly string StoreKey = "migrations";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MigrationsListStore"/> class.
|
||||
/// </summary>
|
||||
public MigrationsListStore()
|
||||
{
|
||||
ConfigurationType = typeof(MigrationOptions);
|
||||
Key = StoreKey;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,10 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class CreateNetworkConfiguration : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ServerApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<CreateNetworkConfiguration> _logger;
|
||||
@ -24,15 +27,6 @@ public class CreateNetworkConfiguration : IMigrationRoutine
|
||||
_logger = loggerFactory.CreateLogger<CreateNetworkConfiguration>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(CreateNetworkConfiguration);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -10,7 +10,10 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateEncodingOptions : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ServerApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<MigrateEncodingOptions> _logger;
|
||||
@ -26,15 +29,6 @@ public class MigrateEncodingOptions : IMigrationRoutine
|
||||
_logger = loggerFactory.CreateLogger<MigrateEncodingOptions>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(MigrateEncodingOptions);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateMusicBrainzTimeout : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ServerApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<MigrateMusicBrainzTimeout> _logger;
|
||||
@ -25,15 +28,6 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
|
||||
_logger = loggerFactory.CreateLogger<MigrateMusicBrainzTimeout>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(MigrateMusicBrainzTimeout);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JellyfinMigration("2025-04-20T01:00:00", nameof(MigrateNetworkConfiguration), "4FB5C950-1991-11EE-9B4B-0800200C9A66", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
|
||||
public class MigrateNetworkConfiguration : IMigrationRoutine
|
||||
{
|
||||
private readonly ServerApplicationPaths _applicationPaths;
|
||||
@ -27,15 +28,6 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
|
||||
_logger = loggerFactory.CreateLogger<MigrateNetworkConfiguration>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("4FB5C950-1991-11EE-9B4B-0800200C9A66");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(MigrateNetworkConfiguration);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Server.Migrations.PreStartupRoutines;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class RenameEnableGroupingIntoCollections : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ServerApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<RenameEnableGroupingIntoCollections> _logger;
|
||||
@ -25,15 +28,6 @@ public class RenameEnableGroupingIntoCollections : IMigrationRoutine
|
||||
_logger = loggerFactory.CreateLogger<RenameEnableGroupingIntoCollections>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => nameof(RenameEnableGroupingIntoCollections);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Migration to add the default cast receivers to the system config.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class AddDefaultCastReceivers : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
@ -20,15 +23,6 @@ public class AddDefaultCastReceivers : IMigrationRoutine
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "AddDefaultCastReceivers";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Migration to initialize system configuration with the default plugin repository.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class AddDefaultPluginRepository : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("EB58EBEE-9514-4B9B-8225-12E1A40020DF");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "AddDefaultPluginRepository";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json,
|
||||
/// otherwise a blank file will be created.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class CreateUserLoggingConfigFile : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
/// <summary>
|
||||
/// File history for logging.json as existed during this migration creation. The contents for each has been minified.
|
||||
@ -42,15 +45,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "CreateLoggingConfigHierarchy";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Disable transcode throttling for all installations since it is currently broken for certain video formats.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class DisableTranscodingThrottling : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ILogger<DisableTranscodingThrottling> _logger;
|
||||
private readonly IConfigurationManager _configManager;
|
||||
@ -18,15 +21,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_configManager = configManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "DisableTranscodingThrottling";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -16,7 +16,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Fixes the data column of audio types to be deserializable.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class FixAudioData : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "library.db";
|
||||
private readonly ILogger<FixAudioData> _logger;
|
||||
@ -33,15 +36,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_logger = loggerFactory.CreateLogger<FixAudioData>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "FixAudioData";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -13,7 +13,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Properly set playlist owner.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class FixPlaylistOwner : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ILogger<FixPlaylistOwner> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -29,15 +32,6 @@ internal class FixPlaylistOwner : IMigrationRoutine
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "FixPlaylistOwner";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -14,7 +14,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// The migration routine for migrating the activity log database to EF Core.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateActivityLogDb : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "activitylog.db";
|
||||
|
||||
@ -35,15 +38,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MigrateActivityLogDatabase";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// A migration that moves data from the authentication database into the new schema.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateAuthenticationDb : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "authentication.db";
|
||||
|
||||
@ -43,15 +46,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MigrateAuthenticationDatabase";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -20,7 +20,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// The migration routine for migrating the display preferences database to EF Core.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateDisplayPreferencesDb : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "displaypreferences.db";
|
||||
|
||||
@ -51,15 +54,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_jsonOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MigrateDisplayPreferencesDatabase";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Migration to move extracted files to the new directories.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData), "EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24")]
|
||||
public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
{
|
||||
private readonly ILogger<MigrateKeyframeData> _logger;
|
||||
@ -44,15 +45,6 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
|
||||
private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MigrateKeyframeData";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -16,10 +16,19 @@ using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
|
||||
using Chapter = Jellyfin.Database.Implementations.Entities.Chapter;
|
||||
@ -29,6 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// The migration routine for migrating the userdata database to EF Core.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb), "36445464-849f-429f-9ad0-bb130efa0664")]
|
||||
internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
{
|
||||
private const string DbFilename = "library.db";
|
||||
@ -45,11 +55,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
/// <param name="provider">The database provider.</param>
|
||||
/// <param name="paths">The server application paths.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
|
||||
/// <param name="serviceProvider">The Service provider.</param>
|
||||
public MigrateLibraryDb(
|
||||
ILogger<MigrateLibraryDb> logger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider)
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_provider = provider;
|
||||
@ -57,15 +69,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MigrateLibraryDbData";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false; // TODO Change back after testing
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
@ -73,6 +76,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
var libraryDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(libraryDbPath))
|
||||
{
|
||||
_logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
|
||||
|
||||
var fullOperationTimer = new Stopwatch();
|
||||
@ -395,8 +404,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
_logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
|
||||
File.Move(libraryDbPath, libraryDbPath + ".old", true);
|
||||
|
||||
_jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private DatabaseMigrationStep GetPreparedDbContext(string operationName)
|
||||
|
@ -10,6 +10,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Migrate rating levels.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels), "98724538-EB11-40E3-931A-252C55BDDE7A")]
|
||||
internal class MigrateRatingLevels : IDatabaseMigrationRoutine
|
||||
{
|
||||
private readonly ILogger<MigrateRatingLevels> _logger;
|
||||
@ -26,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MigrateRatingLevels";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -22,7 +22,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// The migration routine for migrating the user database to EF Core.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MigrateUserDb : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "users.db";
|
||||
|
||||
@ -50,15 +53,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_xmlSerializer = xmlSerializer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "MigrateUserDatabase";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -24,7 +24,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Migration to move extracted files to the new directories.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles), "9063b0Ef-CFF1-4EDC-9A13-74093681A89B")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MoveExtractedFiles : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly ILogger<MoveExtractedFiles> _logger;
|
||||
@ -58,15 +61,6 @@ public class MoveExtractedFiles : IMigrationRoutine
|
||||
|
||||
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MoveExtractedFiles";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Migration to move trickplay files to the new directory.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), "9540D44A-D8DC-11EF-9CBB-B77274F77C52", RunMigrationOnSetup = true)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class MoveTrickplayFiles : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@ -41,15 +44,6 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MoveTrickplayFiles";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
@ -103,7 +97,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
offset += Limit;
|
||||
previousCount = trickplayInfos.Count;
|
||||
|
||||
_logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", itemCount, offset, sw.Elapsed);
|
||||
_logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", offset, itemCount, sw.Elapsed);
|
||||
} while (previousCount == Limit);
|
||||
|
||||
_logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed);
|
||||
|
@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Migration to initialize system configuration with the default plugin repository.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class ReaddDefaultPluginRepository : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "ReaddDefaultPluginRepository";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -8,7 +8,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Removes the old 'RemoveDownloadImagesInAdvance' from library options.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class RemoveDownloadImagesInAdvance : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ILogger<RemoveDownloadImagesInAdvance> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@ -19,15 +22,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "RemoveDownloadImagesInAdvance";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <summary>
|
||||
/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class RemoveDuplicateExtras : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string DbFilename = "library.db";
|
||||
private readonly ILogger<RemoveDuplicateExtras> _logger;
|
||||
@ -24,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "RemoveDuplicateExtras";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -11,7 +11,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Remove duplicate playlist entries.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
@ -24,15 +27,6 @@ internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
|
||||
_playlistManager = playlistManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "RemoveDuplicatePlaylistChildren";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PerformOnNewInstall => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Perform()
|
||||
{
|
||||
|
@ -6,7 +6,10 @@ namespace Jellyfin.Server.Migrations.Routines;
|
||||
/// <summary>
|
||||
/// Migration to update the default Jellyfin plugin repository.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)]
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public class UpdateDefaultPluginRepository : IMigrationRoutine
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
private const string NewRepositoryUrl = "https://repo.jellyfin.org/files/plugin/manifest.json";
|
||||
private const string OldRepositoryUrl = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json";
|
||||
@ -22,15 +25,6 @@ public class UpdateDefaultPluginRepository : IMigrationRoutine
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id => new("852816E0-2712-49A9-9240-C6FC5FCAD1A8");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "UpdateDefaultPluginRepository10.9";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool PerformOnNewInstall => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Perform()
|
||||
{
|
||||
|
51
Jellyfin.Server/Migrations/Stages/CodeMigration.cs
Normal file
51
Jellyfin.Server/Migrations/Stages/CodeMigration.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Stages;
|
||||
|
||||
internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata)
|
||||
{
|
||||
public Type MigrationType { get; } = migrationType;
|
||||
|
||||
public JellyfinMigrationAttribute Metadata { get; } = metadata;
|
||||
|
||||
public string BuildCodeMigrationId()
|
||||
{
|
||||
return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!;
|
||||
}
|
||||
|
||||
public async Task Perform(IServiceProvider? serviceProvider, CancellationToken cancellationToken)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (typeof(IMigrationRoutine).IsAssignableFrom(MigrationType))
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
((IMigrationRoutine)Activator.CreateInstance(MigrationType)!).Perform();
|
||||
}
|
||||
else
|
||||
{
|
||||
((IMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).Perform();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
else if (typeof(IAsyncMigrationRoutine).IsAssignableFrom(MigrationType))
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
await ((IAsyncMigrationRoutine)Activator.CreateInstance(MigrationType)!).PerformAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
namespace Jellyfin.Server.Migrations.Stages;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the stages the <see cref="JellyfinMigrationService"/> supports.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1008 // Enums should have zero value
|
||||
public enum JellyfinMigrationStageTypes
|
||||
#pragma warning restore CA1008 // Enums should have zero value
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs before services are initialised.
|
||||
/// Reserved for migrations that are modifying the application server itself. Should be avoided if possible.
|
||||
/// </summary>
|
||||
PreInitialisation = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
CoreInitialisaition = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Runs after services has been registered and initialised. Last step before running the server.
|
||||
/// </summary>
|
||||
AppInitialisation = 3
|
||||
}
|
16
Jellyfin.Server/Migrations/Stages/MigrationStage.cs
Normal file
16
Jellyfin.Server/Migrations/Stages/MigrationStage.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Stages;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a Stage that can be Invoked and Handled at different times from the code.
|
||||
/// </summary>
|
||||
internal class MigrationStage : Collection<CodeMigration>
|
||||
{
|
||||
public MigrationStage(JellyfinMigrationStageTypes stage)
|
||||
{
|
||||
Stage = stage;
|
||||
}
|
||||
|
||||
public JellyfinMigrationStageTypes Stage { get; }
|
||||
}
|
@ -9,17 +9,20 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Emby.Server.Implementations;
|
||||
using Emby.Server.Implementations.Configuration;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.Helpers;
|
||||
using Jellyfin.Server.Implementations.DatabaseConfiguration;
|
||||
using Jellyfin.Server.Implementations.Extensions;
|
||||
using Jellyfin.Server.Implementations.StorageHelpers;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -126,7 +129,8 @@ namespace Jellyfin.Server
|
||||
StorageHelper.TestCommonPathsForStorageCapacity(appPaths, _loggerFactory.CreateLogger<Startup>());
|
||||
|
||||
StartupHelpers.PerformStaticInitialization();
|
||||
await Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory).ConfigureAwait(false);
|
||||
|
||||
await ApplyStartupMigrationAsync(appPaths, startupConfig).ConfigureAwait(false);
|
||||
|
||||
do
|
||||
{
|
||||
@ -171,9 +175,11 @@ 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;
|
||||
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false);
|
||||
|
||||
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
|
||||
await Migrations.MigrationRunner.Run(appHost, _loggerFactory).ConfigureAwait(false);
|
||||
|
||||
await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
@ -223,6 +229,45 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Internal]Runs the startup Migrations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not intended to be used other then by jellyfin and its tests.
|
||||
/// </remarks>
|
||||
/// <param name="appPaths">Application Paths.</param>
|
||||
/// <param name="startupConfig">Startup Config.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig)
|
||||
{
|
||||
var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer());
|
||||
startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]);
|
||||
var migrationStartupServiceProvider = new ServiceCollection()
|
||||
.AddLogging(d => d.AddSerilog())
|
||||
.AddJellyfinDbContext(startupConfigurationManager, startupConfig)
|
||||
.AddSingleton<IApplicationPaths>(appPaths)
|
||||
.AddSingleton<ServerApplicationPaths>(appPaths);
|
||||
var startupService = migrationStartupServiceProvider.BuildServiceProvider();
|
||||
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(startupService);
|
||||
await jellyfinMigrationService.CheckFirstTimeRunOrMigration(appPaths).ConfigureAwait(false);
|
||||
await jellyfinMigrationService.MigrateStepAsync(Migrations.Stages.JellyfinMigrationStageTypes.PreInitialisation, startupService).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Internal]Runs the Jellyfin migrator service with the Core stage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not intended to be used other then by jellyfin and its tests.
|
||||
/// </remarks>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
/// <param name="jellyfinMigrationStage">The stage to run.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task ApplyCoreMigrationsAsync(IServiceProvider serviceProvider, Migrations.Stages.JellyfinMigrationStageTypes jellyfinMigrationStage)
|
||||
{
|
||||
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(serviceProvider);
|
||||
await jellyfinMigrationService.MigrateStepAsync(jellyfinMigrationStage, serviceProvider).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the application configuration.
|
||||
/// </summary>
|
||||
|
@ -76,6 +76,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
|
||||
/// <inheritdoc/>
|
||||
public async Task RunShutdownTask(CancellationToken cancellationToken)
|
||||
{
|
||||
if (DbContextFactory is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Run before disposing the application
|
||||
var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
|
@ -6,6 +6,7 @@ using Emby.Server.Implementations;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.Helpers;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -103,7 +104,11 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var host = builder.Build();
|
||||
var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>();
|
||||
appHost.ServiceProvider = host.Services;
|
||||
var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>();
|
||||
Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult();
|
||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).GetAwaiter().GetResult();
|
||||
appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult();
|
||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
|
||||
host.Start();
|
||||
|
||||
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
|
||||
|
Loading…
x
Reference in New Issue
Block a user