Allow custom plugin provided database providers to be loaded (#14171)

This commit is contained in:
JPVenson 2025-06-04 01:53:37 +03:00 committed by GitHub
parent d5672ce407
commit 916e897ed2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 97 additions and 3 deletions

View file

@ -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);
}
/// <summary>
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
@ -55,7 +79,6 @@ public static class ServiceCollectionExtensions
IConfiguration configuration)
{
var efCoreConfiguration = configurationManager.GetConfiguration<DatabaseConfigurationOptions>("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<IJellyfinDatabaseProvider>(providerFactory!);

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Jellyfin.Database.Implementations.DbConfiguration;
/// <summary>
/// The custom value option for custom database providers.
/// </summary>
public class CustomDatabaseOption
{
/// <summary>
/// Gets or sets the key of the value.
/// </summary>
public required string Key { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
public required string Value { get; set; }
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Jellyfin.Database.Implementations.DbConfiguration;
/// <summary>
/// Defines the options for a custom database connector.
/// </summary>
public class CustomDatabaseOptions
{
/// <summary>
/// Gets or sets the Plugin name to search for database providers.
/// </summary>
public required string PluginName { get; set; }
/// <summary>
/// Gets or sets the plugin assembly to search for providers.
/// </summary>
public required string PluginAssembly { get; set; }
/// <summary>
/// Gets or sets the connection string for the custom database provider.
/// </summary>
public required string ConnectionString { get; set; }
/// <summary>
/// Gets or sets the list of extra options for the custom provider.
/// </summary>
#pragma warning disable CA2227 // Collection properties should be read only
public Collection<CustomDatabaseOption> Options { get; set; } = [];
#pragma warning restore CA2227 // Collection properties should be read only
}

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Jellyfin.Database.Implementations.DbConfiguration;
/// <summary>
@ -10,6 +12,11 @@ public class DatabaseConfigurationOptions
/// </summary>
public required string DatabaseType { get; set; }
/// <summary>
/// Gets or sets the options required to use a custom database provider.
/// </summary>
public CustomDatabaseOptions? CustomProviderOptions { get; set; }
/// <summary>
/// Gets or Sets the kind of locking behavior jellyfin should perform. Possible options are "NoLock", "Pessimistic", "Optimistic".
/// Defaults to "NoLock".