From 916e897ed25f74b0112e51758e673f18ad13af1d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 4 Jun 2025 01:53:37 +0300 Subject: [PATCH] Allow custom plugin provided database providers to be loaded (#14171) --- .../Extensions/ServiceCollectionExtensions.cs | 42 +++++++++++++++++-- .../DbConfiguration/CustomDatabaseOption.cs | 19 +++++++++ .../DbConfiguration/CustomDatabaseOptions.cs | 32 ++++++++++++++ .../DatabaseConfigurationOptions.cs | 7 ++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 392a8de2cb..63c80634f6 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Reflection; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; @@ -42,6 +44,28 @@ public static class ServiceCollectionExtensions return items; } + private static JellyfinDbProviderFactory? LoadDatabasePlugin(CustomDatabaseOptions customProviderOptions, IApplicationPaths applicationPaths) + { + var plugin = Directory.EnumerateDirectories(applicationPaths.PluginsPath) + .Where(e => Path.GetFileName(e)!.StartsWith(customProviderOptions.PluginName, StringComparison.OrdinalIgnoreCase)) + .Order() + .FirstOrDefault() + ?? throw new InvalidOperationException($"The requested custom database plugin with the name '{customProviderOptions.PluginName}' could not been found in '{applicationPaths.PluginsPath}'"); + + var dbProviderAssembly = Path.Combine(plugin, Path.ChangeExtension(customProviderOptions.PluginAssembly, "dll")); + if (!File.Exists(dbProviderAssembly)) + { + throw new InvalidOperationException($"Could not find the requested assembly at '{dbProviderAssembly}'"); + } + + // we have to load the assembly without proxy to ensure maximum performance for this. + var assembly = Assembly.LoadFrom(dbProviderAssembly); + var dbProviderType = assembly.GetExportedTypes().FirstOrDefault(f => f.IsAssignableTo(typeof(IJellyfinDatabaseProvider))) + ?? throw new InvalidOperationException($"Could not find any type implementing the '{nameof(IJellyfinDatabaseProvider)}' interface."); + + return (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, dbProviderType); + } + /// /// Adds the interface to the service collection with second level caching enabled. /// @@ -55,7 +79,6 @@ public static class ServiceCollectionExtensions IConfiguration configuration) { var efCoreConfiguration = configurationManager.GetConfiguration("database"); - var providers = GetSupportedDbProviders(); JellyfinDbProviderFactory? providerFactory = null; if (efCoreConfiguration?.DatabaseType is null) @@ -80,9 +103,22 @@ public static class ServiceCollectionExtensions } } - if (!providers.TryGetValue(efCoreConfiguration.DatabaseType.ToUpperInvariant(), out providerFactory!)) + if (efCoreConfiguration.DatabaseType.Equals("PLUGIN_PROVIDER", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}"); + if (efCoreConfiguration.CustomProviderOptions is null) + { + throw new InvalidOperationException("The custom database provider must declare the custom provider options to work"); + } + + providerFactory = LoadDatabasePlugin(efCoreConfiguration.CustomProviderOptions, configurationManager.ApplicationPaths); + } + else + { + var providers = GetSupportedDbProviders(); + if (!providers.TryGetValue(efCoreConfiguration.DatabaseType.ToUpperInvariant(), out providerFactory!)) + { + throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}"); + } } serviceCollection.AddSingleton(providerFactory!); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs new file mode 100644 index 0000000000..fcb8f41b34 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Jellyfin.Database.Implementations.DbConfiguration; + +/// +/// The custom value option for custom database providers. +/// +public class CustomDatabaseOption +{ + /// + /// Gets or sets the key of the value. + /// + public required string Key { get; set; } + + /// + /// Gets or sets the value. + /// + public required string Value { get; set; } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs new file mode 100644 index 0000000000..e2088704dc --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Jellyfin.Database.Implementations.DbConfiguration; + +/// +/// Defines the options for a custom database connector. +/// +public class CustomDatabaseOptions +{ + /// + /// Gets or sets the Plugin name to search for database providers. + /// + public required string PluginName { get; set; } + + /// + /// Gets or sets the plugin assembly to search for providers. + /// + public required string PluginAssembly { get; set; } + + /// + /// Gets or sets the connection string for the custom database provider. + /// + public required string ConnectionString { get; set; } + + /// + /// Gets or sets the list of extra options for the custom provider. + /// +#pragma warning disable CA2227 // Collection properties should be read only + public Collection Options { get; set; } = []; +#pragma warning restore CA2227 // Collection properties should be read only +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs index 682e5019bb..bc0cacf3c3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Jellyfin.Database.Implementations.DbConfiguration; /// @@ -10,6 +12,11 @@ public class DatabaseConfigurationOptions /// public required string DatabaseType { get; set; } + /// + /// Gets or sets the options required to use a custom database provider. + /// + public CustomDatabaseOptions? CustomProviderOptions { get; set; } + /// /// Gets or Sets the kind of locking behavior jellyfin should perform. Possible options are "NoLock", "Pessimistic", "Optimistic". /// Defaults to "NoLock".