using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Locking; using Jellyfin.Database.Providers.Sqlite; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using JellyfinDbProviderFactory = System.Func; namespace Jellyfin.Server.Implementations.Extensions; /// /// Extensions for the interface. /// public static class ServiceCollectionExtensions { private static IEnumerable DatabaseProviderTypes() { yield return typeof(SqliteDatabaseProvider); } private static IDictionary GetSupportedDbProviders() { var items = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var providerType in DatabaseProviderTypes()) { var keyAttribute = providerType.GetCustomAttribute(); if (keyAttribute is null || string.IsNullOrWhiteSpace(keyAttribute.DatabaseProviderKey)) { continue; } var provider = providerType; items[keyAttribute.DatabaseProviderKey] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); } 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. /// /// An instance of the interface. /// The server configuration manager. /// The startup Configuration. /// The updated service collection. public static IServiceCollection AddJellyfinDbContext( this IServiceCollection serviceCollection, IServerConfigurationManager configurationManager, IConfiguration configuration) { var efCoreConfiguration = configurationManager.GetConfiguration("database"); JellyfinDbProviderFactory? providerFactory = null; if (efCoreConfiguration?.DatabaseType is null) { var cmdMigrationArgument = configuration.GetValue("migration-provider"); if (!string.IsNullOrWhiteSpace(cmdMigrationArgument)) { efCoreConfiguration = new DatabaseConfigurationOptions() { DatabaseType = cmdMigrationArgument, }; } else { // when nothing is setup via new Database configuration, fallback to SQLite with default settings. efCoreConfiguration = new DatabaseConfigurationOptions() { DatabaseType = "Jellyfin-SQLite", LockingBehavior = DatabaseLockingBehaviorTypes.NoLock }; configurationManager.SaveConfiguration("database", efCoreConfiguration); } } if (efCoreConfiguration.DatabaseType.Equals("PLUGIN_PROVIDER", StringComparison.OrdinalIgnoreCase)) { 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!); switch (efCoreConfiguration.LockingBehavior) { case DatabaseLockingBehaviorTypes.NoLock: serviceCollection.AddSingleton(); break; case DatabaseLockingBehaviorTypes.Pessimistic: serviceCollection.AddSingleton(); break; case DatabaseLockingBehaviorTypes.Optimistic: serviceCollection.AddSingleton(); break; } serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => { var provider = serviceProvider.GetRequiredService(); provider.Initialise(opt); var lockingBehavior = serviceProvider.GetRequiredService(); lockingBehavior.Initialise(opt); }); return serviceCollection; } }