< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.AppBase.BaseConfigurationManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
Line coverage
65%
Covered lines: 83
Uncovered lines: 43
Coverable lines: 126
Total lines: 381
Line coverage: 65.8%
Branch coverage
36%
Covered branches: 13
Total branches: 36
Branch coverage: 36.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using MediaBrowser.Common.Configuration;
 8using MediaBrowser.Common.Events;
 9using MediaBrowser.Common.Extensions;
 10using MediaBrowser.Model.Configuration;
 11using MediaBrowser.Model.Serialization;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Emby.Server.Implementations.AppBase
 15{
 16    /// <summary>
 17    /// Class BaseConfigurationManager.
 18    /// </summary>
 19    public abstract class BaseConfigurationManager : IConfigurationManager
 20    {
 2221        private readonly ConcurrentDictionary<string, object> _configurations = new();
 2222        private readonly object _configurationSyncLock = new();
 23
 2224        private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
 2225        private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
 26
 27        /// <summary>
 28        /// The _configuration.
 29        /// </summary>
 30        private BaseApplicationConfiguration? _configuration;
 31
 32        /// <summary>
 33        /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
 34        /// </summary>
 35        /// <param name="applicationPaths">The application paths.</param>
 36        /// <param name="loggerFactory">The logger factory.</param>
 37        /// <param name="xmlSerializer">The XML serializer.</param>
 38        protected BaseConfigurationManager(
 39            IApplicationPaths applicationPaths,
 40            ILoggerFactory loggerFactory,
 41            IXmlSerializer xmlSerializer)
 42        {
 2243            CommonApplicationPaths = applicationPaths;
 2244            XmlSerializer = xmlSerializer;
 2245            Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
 46
 2247            UpdateCachePath();
 2248        }
 49
 50        /// <summary>
 51        /// Occurs when [configuration updated].
 52        /// </summary>
 53        public event EventHandler<EventArgs>? ConfigurationUpdated;
 54
 55        /// <summary>
 56        /// Occurs when [configuration updating].
 57        /// </summary>
 58        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
 59
 60        /// <summary>
 61        /// Occurs when [named configuration updated].
 62        /// </summary>
 63        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
 64
 65        /// <summary>
 66        /// Gets the type of the configuration.
 67        /// </summary>
 68        /// <value>The type of the configuration.</value>
 69        protected abstract Type ConfigurationType { get; }
 70
 71        /// <summary>
 72        /// Gets the logger.
 73        /// </summary>
 74        /// <value>The logger.</value>
 75        protected ILogger<BaseConfigurationManager> Logger { get; private set; }
 76
 77        /// <summary>
 78        /// Gets the XML serializer.
 79        /// </summary>
 80        /// <value>The XML serializer.</value>
 81        protected IXmlSerializer XmlSerializer { get; private set; }
 82
 83        /// <summary>
 84        /// Gets the application paths.
 85        /// </summary>
 86        /// <value>The application paths.</value>
 87        public IApplicationPaths CommonApplicationPaths { get; private set; }
 88
 89        /// <summary>
 90        /// Gets or sets the system configuration.
 91        /// </summary>
 92        /// <value>The configuration.</value>
 93        public BaseApplicationConfiguration CommonConfiguration
 94        {
 95            get
 96            {
 325497                if (_configuration is not null)
 98                {
 323299                    return _configuration;
 100                }
 101
 22102                lock (_configurationSyncLock)
 103                {
 22104                    if (_configuration is not null)
 105                    {
 0106                        return _configuration;
 107                    }
 108
 22109                    return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(Config
 110                }
 22111            }
 112
 113            protected set
 114            {
 0115                _configuration = value;
 0116            }
 117        }
 118
 119        /// <summary>
 120        /// Manually pre-loads a factory so that it is available pre system initialisation.
 121        /// </summary>
 122        /// <typeparam name="T">Class to register.</typeparam>
 123        public virtual void RegisterConfiguration<T>()
 124            where T : IConfigurationFactory
 125        {
 0126            IConfigurationFactory factory = Activator.CreateInstance<T>();
 127
 0128            if (_configurationFactories is null)
 129            {
 0130                _configurationFactories = [factory];
 131            }
 132            else
 133            {
 0134                _configurationFactories = [.._configurationFactories, factory];
 135            }
 136
 0137            _configurationStores = _configurationFactories
 0138                .SelectMany(i => i.GetConfigurations())
 0139                .ToArray();
 0140        }
 141
 142        /// <summary>
 143        /// Adds parts.
 144        /// </summary>
 145        /// <param name="factories">The configuration factories.</param>
 146        public virtual void AddParts(IEnumerable<IConfigurationFactory> factories)
 147        {
 22148            _configurationFactories = factories.ToArray();
 149
 22150            _configurationStores = _configurationFactories
 22151                .SelectMany(i => i.GetConfigurations())
 22152                .ToArray();
 22153        }
 154
 155        /// <summary>
 156        /// Saves the configuration.
 157        /// </summary>
 158        public void SaveConfiguration()
 159        {
 40160            Logger.LogInformation("Saving system configuration");
 40161            var path = CommonApplicationPaths.SystemConfigurationFilePath;
 162
 40163            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be 
 164
 40165            lock (_configurationSyncLock)
 166            {
 40167                XmlSerializer.SerializeToFile(CommonConfiguration, path);
 40168            }
 169
 40170            OnConfigurationUpdated();
 40171        }
 172
 173        /// <summary>
 174        /// Called when [configuration updated].
 175        /// </summary>
 176        protected virtual void OnConfigurationUpdated()
 177        {
 40178            UpdateCachePath();
 179
 40180            EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger);
 40181        }
 182
 183        /// <summary>
 184        /// Replaces the configuration.
 185        /// </summary>
 186        /// <param name="newConfiguration">The new configuration.</param>
 187        /// <exception cref="ArgumentNullException"><c>newConfiguration</c> is <c>null</c>.</exception>
 188        public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
 189        {
 0190            ArgumentNullException.ThrowIfNull(newConfiguration);
 191
 0192            ValidateCachePath(newConfiguration);
 193
 0194            CommonConfiguration = newConfiguration;
 0195            SaveConfiguration();
 0196        }
 197
 198        /// <summary>
 199        /// Updates the items by name path.
 200        /// </summary>
 201        private void UpdateCachePath()
 202        {
 203            string cachePath;
 204
 205            // If the configuration file has no entry (i.e. not set in UI)
 62206            if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
 207            {
 208                // If the current live configuration has no entry (i.e. not set on CLI/envvars, during startup)
 62209                if (string.IsNullOrWhiteSpace(((BaseApplicationPaths)CommonApplicationPaths).CachePath))
 210                {
 211                    // Set cachePath to a default value under ProgramDataPath
 0212                    cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache");
 213                }
 214                else
 215                {
 216                    // Set cachePath to the existing live value; will require restart if UI value is removed (but not re
 217                    // TODO: Figure out how to re-grab this from the CLI/envvars while running
 62218                    cachePath = ((BaseApplicationPaths)CommonApplicationPaths).CachePath;
 219                }
 220            }
 221            else
 222            {
 223                // Set cachePath to the new UI-set value
 0224                cachePath = CommonConfiguration.CachePath;
 225            }
 226
 62227            Logger.LogInformation("Setting cache path: {Path}", cachePath);
 62228            ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
 62229        }
 230
 231        /// <summary>
 232        /// Replaces the cache path.
 233        /// </summary>
 234        /// <param name="newConfig">The new configuration.</param>
 235        /// <exception cref="DirectoryNotFoundException">The new cache path doesn't exist.</exception>
 236        private void ValidateCachePath(BaseApplicationConfiguration newConfig)
 237        {
 0238            var newPath = newConfig.CachePath;
 239
 0240            if (!string.IsNullOrWhiteSpace(newPath)
 0241                && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal))
 242            {
 243                // Validate
 0244                if (!Directory.Exists(newPath))
 245                {
 0246                    throw new DirectoryNotFoundException(
 0247                        string.Format(
 0248                            CultureInfo.InvariantCulture,
 0249                            "{0} does not exist.",
 0250                            newPath));
 251                }
 252
 0253                EnsureWriteAccess(newPath);
 254            }
 0255        }
 256
 257        /// <summary>
 258        /// Ensures that we have write access to the path.
 259        /// </summary>
 260        /// <param name="path">The path.</param>
 261        protected void EnsureWriteAccess(string path)
 262        {
 0263            var file = Path.Combine(path, Guid.NewGuid().ToString());
 0264            File.WriteAllText(file, string.Empty);
 0265            File.Delete(file);
 0266        }
 267
 268        private string GetConfigurationFile(string key)
 269        {
 71270            return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml");
 271        }
 272
 273        /// <inheritdoc />
 274        public object GetConfiguration(string key)
 275        {
 1057276            return _configurations.GetOrAdd(
 1057277                key,
 1057278                static (k, configurationManager) =>
 1057279                {
 1057280                    var file = configurationManager.GetConfigurationFile(k);
 1057281
 1057282                    var configurationInfo = Array.Find(
 1057283                        configurationManager._configurationStores,
 1057284                        i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
 1057285
 1057286                    if (configurationInfo is null)
 1057287                    {
 1057288                        throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
 1057289                    }
 1057290
 1057291                    var configurationType = configurationInfo.ConfigurationType;
 1057292
 1057293                    lock (configurationManager._configurationSyncLock)
 1057294                    {
 1057295                        return configurationManager.LoadConfiguration(file, configurationType);
 1057296                    }
 1057297                },
 1057298                this);
 299        }
 300
 301        private object LoadConfiguration(string path, Type configurationType)
 302        {
 303            try
 304            {
 70305                if (File.Exists(path))
 306                {
 0307                    return XmlSerializer.DeserializeFromFile(configurationType, path);
 308                }
 70309            }
 0310            catch (Exception ex) when (ex is not IOException)
 311            {
 0312                Logger.LogError(ex, "Error loading configuration file: {Path}", path);
 0313            }
 314
 70315            return Activator.CreateInstance(configurationType)
 70316                ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
 0317        }
 318
 319        /// <inheritdoc />
 320        public void SaveConfiguration(string key, object configuration)
 321        {
 1322            var configurationStore = GetConfigurationStore(key);
 1323            var configurationType = configurationStore.ConfigurationType;
 324
 1325            if (configuration.GetType() != configurationType)
 326            {
 0327                throw new ArgumentException("Expected configuration type is " + configurationType.Name);
 328            }
 329
 1330            if (configurationStore is IValidatingConfiguration validatingStore)
 331            {
 0332                var currentConfiguration = GetConfiguration(key);
 333
 0334                validatingStore.Validate(currentConfiguration, configuration);
 335            }
 336
 1337            NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
 338
 1339            _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
 340
 1341            var path = GetConfigurationFile(key);
 1342            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be 
 343
 1344            lock (_configurationSyncLock)
 345            {
 1346                XmlSerializer.SerializeToFile(configuration, path);
 1347            }
 348
 1349            OnNamedConfigurationUpdated(key, configuration);
 1350        }
 351
 352        /// <summary>
 353        /// Event handler for when a named configuration has been updated.
 354        /// </summary>
 355        /// <param name="key">The key of the configuration.</param>
 356        /// <param name="configuration">The old configuration.</param>
 357        protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
 358        {
 1359            NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
 1360        }
 361
 362        /// <inheritdoc />
 363        public ConfigurationStore[] GetConfigurationStores()
 364        {
 1365            return _configurationStores;
 366        }
 367
 368        /// <inheritdoc />
 369        public Type GetConfigurationType(string key)
 370        {
 0371            return GetConfigurationStore(key)
 0372                .ConfigurationType;
 373        }
 374
 375        private ConfigurationStore GetConfigurationStore(string key)
 376        {
 1377            return _configurationStores
 1378                .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
 379        }
 380    }
 381}