Quellcode durchsuchen

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

JPVenson vor 2 Tagen
Ursprung
Commit
916e897ed2

+ 39 - 3
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);
+    }
+
     /// <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!);

+ 19 - 0
src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs

@@ -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; }
+}

+ 32 - 0
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;
+
+/// <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
+}

+ 7 - 0
src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs

@@ -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".