#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.AppBase
{
///
/// Class BaseConfigurationManager.
///
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly IFileSystem _fileSystem;
private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary();
///
/// The _configuration sync lock.
///
private readonly object _configurationSyncLock = new object();
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
///
/// The _configuration loaded.
///
private bool _configurationLoaded;
///
/// The _configuration.
///
private BaseApplicationConfiguration _configuration;
///
/// Initializes a new instance of the class.
///
/// The application paths.
/// The logger factory.
/// The XML serializer.
/// The file system.
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger();
UpdateCachePath();
}
///
/// Occurs when [configuration updated].
///
public event EventHandler ConfigurationUpdated;
///
/// Occurs when [configuration updating].
///
public event EventHandler NamedConfigurationUpdating;
///
/// Occurs when [named configuration updated].
///
public event EventHandler NamedConfigurationUpdated;
///
/// Gets the type of the configuration.
///
/// The type of the configuration.
protected abstract Type ConfigurationType { get; }
///
/// Gets the logger.
///
/// The logger.
protected ILogger Logger { get; private set; }
///
/// Gets the XML serializer.
///
/// The XML serializer.
protected IXmlSerializer XmlSerializer { get; private set; }
///
/// Gets the application paths.
///
/// The application paths.
public IApplicationPaths CommonApplicationPaths { get; private set; }
///
/// Gets or sets the system configuration.
///
/// The configuration.
public BaseApplicationConfiguration CommonConfiguration
{
get
{
if (_configurationLoaded)
{
return _configuration;
}
lock (_configurationSyncLock)
{
if (_configurationLoaded)
{
return _configuration;
}
_configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
_configurationLoaded = true;
return _configuration;
}
}
protected set
{
_configuration = value;
_configurationLoaded = value != null;
}
}
///
/// Manually pre-loads a factory so that it is available pre system initialisation.
///
/// Class to register.
public virtual void RegisterConfiguration()
where T : IConfigurationFactory
{
IConfigurationFactory factory = Activator.CreateInstance();
if (_configurationFactories == null)
{
_configurationFactories = new[] { factory };
}
else
{
var oldLen = _configurationFactories.Length;
var arr = new IConfigurationFactory[oldLen + 1];
_configurationFactories.CopyTo(arr, 0);
arr[oldLen] = factory;
_configurationFactories = arr;
}
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
///
/// Adds parts.
///
/// The configuration factories.
public virtual void AddParts(IEnumerable factories)
{
_configurationFactories = factories.ToArray();
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
///
/// Saves the configuration.
///
public void SaveConfiguration()
{
Logger.LogInformation("Saving system configuration");
var path = CommonApplicationPaths.SystemConfigurationFilePath;
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_configurationSyncLock)
{
XmlSerializer.SerializeToFile(CommonConfiguration, path);
}
OnConfigurationUpdated();
}
///
/// Called when [configuration updated].
///
protected virtual void OnConfigurationUpdated()
{
UpdateCachePath();
EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger);
}
///
/// Replaces the configuration.
///
/// The new configuration.
/// newConfiguration is null.
public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
{
if (newConfiguration == null)
{
throw new ArgumentNullException(nameof(newConfiguration));
}
ValidateCachePath(newConfiguration);
CommonConfiguration = newConfiguration;
SaveConfiguration();
}
///
/// Updates the items by name path.
///
private void UpdateCachePath()
{
string cachePath;
// If the configuration file has no entry (i.e. not set in UI)
if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
{
// If the current live configuration has no entry (i.e. not set on CLI/envvars, during startup)
if (string.IsNullOrWhiteSpace(((BaseApplicationPaths)CommonApplicationPaths).CachePath))
{
// Set cachePath to a default value under ProgramDataPath
cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache");
}
else
{
// Set cachePath to the existing live value; will require restart if UI value is removed (but not replaced)
// TODO: Figure out how to re-grab this from the CLI/envvars while running
cachePath = ((BaseApplicationPaths)CommonApplicationPaths).CachePath;
}
}
else
{
// Set cachePath to the new UI-set value
cachePath = CommonConfiguration.CachePath;
}
Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
}
///
/// Replaces the cache path.
///
/// The new configuration.
/// The new cache path doesn't exist.
private void ValidateCachePath(BaseApplicationConfiguration newConfig)
{
var newPath = newConfig.CachePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal))
{
// Validate
if (!Directory.Exists(newPath))
{
throw new DirectoryNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"{0} does not exist.",
newPath));
}
EnsureWriteAccess(newPath);
}
}
///
/// Ensures that we have write access to the path.
///
/// The path.
protected void EnsureWriteAccess(string path)
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
_fileSystem.DeleteFile(file);
}
private string GetConfigurationFile(string key)
{
return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml");
}
///
public object GetConfiguration(string key)
{
return _configurations.GetOrAdd(
key,
static (k, configurationManager) =>
{
var file = configurationManager.GetConfigurationFile(k);
var configurationInfo = Array.Find(
configurationManager._configurationStores,
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
if (configurationInfo == null)
{
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
}
var configurationType = configurationInfo.ConfigurationType;
lock (configurationManager._configurationSyncLock)
{
return configurationManager.LoadConfiguration(file, configurationType);
}
},
this);
}
private object LoadConfiguration(string path, Type configurationType)
{
if (!File.Exists(path))
{
return Activator.CreateInstance(configurationType);
}
try
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
}
catch (IOException)
{
return Activator.CreateInstance(configurationType);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
return Activator.CreateInstance(configurationType);
}
}
///
public void SaveConfiguration(string key, object configuration)
{
var configurationStore = GetConfigurationStore(key);
var configurationType = configurationStore.ConfigurationType;
if (configuration.GetType() != configurationType)
{
throw new ArgumentException("Expected configuration type is " + configurationType.Name);
}
if (configurationStore is IValidatingConfiguration validatingStore)
{
var currentConfiguration = GetConfiguration(key);
validatingStore.Validate(currentConfiguration, configuration);
}
NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs
{
Key = key,
NewConfiguration = configuration
});
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
var path = GetConfigurationFile(key);
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_configurationSyncLock)
{
XmlSerializer.SerializeToFile(configuration, path);
}
OnNamedConfigurationUpdated(key, configuration);
}
///
/// Event handler for when a named configuration has been updated.
///
/// The key of the configuration.
/// The old configuration.
protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
{
NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs
{
Key = key,
NewConfiguration = configuration
});
}
///
public ConfigurationStore[] GetConfigurationStores()
{
return _configurationStores;
}
///
public Type GetConfigurationType(string key)
{
return GetConfigurationStore(key)
.ConfigurationType;
}
private ConfigurationStore GetConfigurationStore(string key)
{
return _configurationStores
.First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
}
}
}