< 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
66%
Covered lines: 83
Uncovered lines: 41
Coverable lines: 124
Total lines: 383
Line coverage: 66.9%
Branch coverage
38%
Covered branches: 14
Total branches: 36
Branch coverage: 38.8%
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 System.Threading;
 8using MediaBrowser.Common.Configuration;
 9using MediaBrowser.Common.Events;
 10using MediaBrowser.Common.Extensions;
 11using MediaBrowser.Model.Configuration;
 12using MediaBrowser.Model.Serialization;
 13using Microsoft.Extensions.Logging;
 14
 15namespace Emby.Server.Implementations.AppBase
 16{
 17    /// <summary>
 18    /// Class BaseConfigurationManager.
 19    /// </summary>
 20    public abstract class BaseConfigurationManager : IConfigurationManager
 21    {
 4222        private readonly ConcurrentDictionary<string, object> _configurations = new();
 4223        private readonly Lock _configurationSyncLock = new();
 24
 4225        private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
 4226        private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
 27
 28        /// <summary>
 29        /// The _configuration.
 30        /// </summary>
 31        private BaseApplicationConfiguration? _configuration;
 32
 33        /// <summary>
 34        /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
 35        /// </summary>
 36        /// <param name="applicationPaths">The application paths.</param>
 37        /// <param name="loggerFactory">The logger factory.</param>
 38        /// <param name="xmlSerializer">The XML serializer.</param>
 39        protected BaseConfigurationManager(
 40            IApplicationPaths applicationPaths,
 41            ILoggerFactory loggerFactory,
 42            IXmlSerializer xmlSerializer)
 43        {
 4244            CommonApplicationPaths = applicationPaths;
 4245            XmlSerializer = xmlSerializer;
 4246            Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
 47
 4248            UpdateCachePath();
 4249        }
 50
 51        /// <summary>
 52        /// Occurs when [configuration updated].
 53        /// </summary>
 54        public event EventHandler<EventArgs>? ConfigurationUpdated;
 55
 56        /// <summary>
 57        /// Occurs when [configuration updating].
 58        /// </summary>
 59        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
 60
 61        /// <summary>
 62        /// Occurs when [named configuration updated].
 63        /// </summary>
 64        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
 65
 66        /// <summary>
 67        /// Gets the type of the configuration.
 68        /// </summary>
 69        /// <value>The type of the configuration.</value>
 70        protected abstract Type ConfigurationType { get; }
 71
 72        /// <summary>
 73        /// Gets the logger.
 74        /// </summary>
 75        /// <value>The logger.</value>
 76        protected ILogger<BaseConfigurationManager> Logger { get; private set; }
 77
 78        /// <summary>
 79        /// Gets the XML serializer.
 80        /// </summary>
 81        /// <value>The XML serializer.</value>
 82        protected IXmlSerializer XmlSerializer { get; private set; }
 83
 84        /// <summary>
 85        /// Gets the application paths.
 86        /// </summary>
 87        /// <value>The application paths.</value>
 88        public IApplicationPaths CommonApplicationPaths { get; private set; }
 89
 90        /// <summary>
 91        /// Gets or sets the system configuration.
 92        /// </summary>
 93        /// <value>The configuration.</value>
 94        public BaseApplicationConfiguration CommonConfiguration
 95        {
 96            get
 97            {
 468398                if (_configuration is not null)
 99                {
 4641100                    return _configuration;
 101                }
 102
 103                lock (_configurationSyncLock)
 104                {
 42105                    if (_configuration is not null)
 106                    {
 0107                        return _configuration;
 108                    }
 109
 42110                    return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(Config
 111                }
 42112            }
 113
 114            protected set
 115            {
 0116                _configuration = value;
 0117            }
 118        }
 119
 120        /// <summary>
 121        /// Manually pre-loads a factory so that it is available pre system initialisation.
 122        /// </summary>
 123        /// <typeparam name="T">Class to register.</typeparam>
 124        public virtual void RegisterConfiguration<T>()
 125            where T : IConfigurationFactory
 126        {
 0127            IConfigurationFactory factory = Activator.CreateInstance<T>();
 128
 0129            if (_configurationFactories is null)
 130            {
 0131                _configurationFactories = [factory];
 132            }
 133            else
 134            {
 0135                _configurationFactories = [.._configurationFactories, factory];
 136            }
 137
 0138            _configurationStores = _configurationFactories
 0139                .SelectMany(i => i.GetConfigurations())
 0140                .ToArray();
 0141        }
 142
 143        /// <summary>
 144        /// Adds parts.
 145        /// </summary>
 146        /// <param name="factories">The configuration factories.</param>
 147        public virtual void AddParts(IEnumerable<IConfigurationFactory> factories)
 148        {
 42149            _configurationFactories = factories.ToArray();
 150
 42151            _configurationStores = _configurationFactories
 42152                .SelectMany(i => i.GetConfigurations())
 42153                .ToArray();
 42154        }
 155
 156        /// <summary>
 157        /// Saves the configuration.
 158        /// </summary>
 159        public void SaveConfiguration()
 160        {
 101161            Logger.LogInformation("Saving system configuration");
 101162            var path = CommonApplicationPaths.SystemConfigurationFilePath;
 163
 101164            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be 
 165
 166            lock (_configurationSyncLock)
 167            {
 101168                XmlSerializer.SerializeToFile(CommonConfiguration, path);
 101169            }
 170
 101171            OnConfigurationUpdated();
 101172        }
 173
 174        /// <summary>
 175        /// Called when [configuration updated].
 176        /// </summary>
 177        protected virtual void OnConfigurationUpdated()
 178        {
 101179            UpdateCachePath();
 180
 101181            EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger);
 101182        }
 183
 184        /// <summary>
 185        /// Replaces the configuration.
 186        /// </summary>
 187        /// <param name="newConfiguration">The new configuration.</param>
 188        /// <exception cref="ArgumentNullException"><c>newConfiguration</c> is <c>null</c>.</exception>
 189        public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
 190        {
 0191            ArgumentNullException.ThrowIfNull(newConfiguration);
 192
 0193            ValidateCachePath(newConfiguration);
 194
 0195            CommonConfiguration = newConfiguration;
 0196            SaveConfiguration();
 0197        }
 198
 199        /// <summary>
 200        /// Updates the items by name path.
 201        /// </summary>
 202        private void UpdateCachePath()
 203        {
 204            string cachePath;
 205
 206            // If the configuration file has no entry (i.e. not set in UI)
 143207            if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
 208            {
 209                // If the current live configuration has no entry (i.e. not set on CLI/envvars, during startup)
 143210                if (string.IsNullOrWhiteSpace(((BaseApplicationPaths)CommonApplicationPaths).CachePath))
 211                {
 212                    // Set cachePath to a default value under ProgramDataPath
 0213                    cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache");
 214                }
 215                else
 216                {
 217                    // Set cachePath to the existing live value; will require restart if UI value is removed (but not re
 218                    // TODO: Figure out how to re-grab this from the CLI/envvars while running
 143219                    cachePath = ((BaseApplicationPaths)CommonApplicationPaths).CachePath;
 220                }
 221            }
 222            else
 223            {
 224                // Set cachePath to the new UI-set value
 0225                cachePath = CommonConfiguration.CachePath;
 226            }
 227
 143228            Logger.LogInformation("Setting cache path: {Path}", cachePath);
 143229            ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
 143230            CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache
 143231        }
 232
 233        /// <summary>
 234        /// Replaces the cache path.
 235        /// </summary>
 236        /// <param name="newConfig">The new configuration.</param>
 237        /// <exception cref="DirectoryNotFoundException">The new cache path doesn't exist.</exception>
 238        private void ValidateCachePath(BaseApplicationConfiguration newConfig)
 239        {
 0240            var newPath = newConfig.CachePath;
 241
 0242            if (!string.IsNullOrWhiteSpace(newPath)
 0243                && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal))
 244            {
 245                // Validate
 0246                if (!Directory.Exists(newPath))
 247                {
 0248                    throw new DirectoryNotFoundException(
 0249                        string.Format(
 0250                            CultureInfo.InvariantCulture,
 0251                            "{0} does not exist.",
 0252                            newPath));
 253                }
 254
 0255                EnsureWriteAccess(newPath);
 256            }
 0257        }
 258
 259        /// <summary>
 260        /// Ensures that we have write access to the path.
 261        /// </summary>
 262        /// <param name="path">The path.</param>
 263        protected void EnsureWriteAccess(string path)
 264        {
 0265            var file = Path.Combine(path, Guid.NewGuid().ToString());
 0266            File.WriteAllText(file, string.Empty);
 0267            File.Delete(file);
 0268        }
 269
 270        private string GetConfigurationFile(string key)
 271        {
 132272            return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml");
 273        }
 274
 275        /// <inheritdoc />
 276        public object GetConfiguration(string key)
 277        {
 887278            return _configurations.GetOrAdd(
 887279                key,
 887280                static (k, configurationManager) =>
 887281                {
 887282                    var file = configurationManager.GetConfigurationFile(k);
 887283
 887284                    var configurationInfo = Array.Find(
 887285                        configurationManager._configurationStores,
 887286                        i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
 887287
 887288                    if (configurationInfo is null)
 887289                    {
 887290                        throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
 887291                    }
 887292
 887293                    var configurationType = configurationInfo.ConfigurationType;
 887294
 887295                    lock (configurationManager._configurationSyncLock)
 887296                    {
 887297                        return configurationManager.LoadConfiguration(file, configurationType);
 887298                    }
 887299                },
 887300                this);
 301        }
 302
 303        private object LoadConfiguration(string path, Type configurationType)
 304        {
 305            try
 306            {
 110307                if (File.Exists(path))
 308                {
 21309                    return XmlSerializer.DeserializeFromFile(configurationType, path);
 310                }
 89311            }
 0312            catch (Exception ex) when (ex is not IOException)
 313            {
 0314                Logger.LogError(ex, "Error loading configuration file: {Path}", path);
 0315            }
 316
 89317            return Activator.CreateInstance(configurationType)
 89318                ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
 21319        }
 320
 321        /// <inheritdoc />
 322        public void SaveConfiguration(string key, object configuration)
 323        {
 22324            var configurationStore = GetConfigurationStore(key);
 22325            var configurationType = configurationStore.ConfigurationType;
 326
 22327            if (configuration.GetType() != configurationType)
 328            {
 0329                throw new ArgumentException("Expected configuration type is " + configurationType.Name);
 330            }
 331
 22332            if (configurationStore is IValidatingConfiguration validatingStore)
 333            {
 0334                var currentConfiguration = GetConfiguration(key);
 335
 0336                validatingStore.Validate(currentConfiguration, configuration);
 337            }
 338
 22339            NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
 340
 22341            _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
 342
 22343            var path = GetConfigurationFile(key);
 22344            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be 
 345
 346            lock (_configurationSyncLock)
 347            {
 22348                XmlSerializer.SerializeToFile(configuration, path);
 22349            }
 350
 22351            OnNamedConfigurationUpdated(key, configuration);
 22352        }
 353
 354        /// <summary>
 355        /// Event handler for when a named configuration has been updated.
 356        /// </summary>
 357        /// <param name="key">The key of the configuration.</param>
 358        /// <param name="configuration">The old configuration.</param>
 359        protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
 360        {
 22361            NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
 22362        }
 363
 364        /// <inheritdoc />
 365        public ConfigurationStore[] GetConfigurationStores()
 366        {
 1367            return _configurationStores;
 368        }
 369
 370        /// <inheritdoc />
 371        public Type GetConfigurationType(string key)
 372        {
 0373            return GetConfigurationStore(key)
 0374                .ConfigurationType;
 375        }
 376
 377        private ConfigurationStore GetConfigurationStore(string key)
 378        {
 22379            return _configurationStores
 22380                .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
 381        }
 382    }
 383}