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

382 lines
14 KiB
C#
Raw Normal View History

using System;
2016-10-29 05:40:15 +00:00
using System.Collections.Concurrent;
using System.Collections.Generic;
2019-08-09 21:50:40 +00:00
using System.Globalization;
2016-10-29 05:40:15 +00:00
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.Serialization;
using Microsoft.Extensions.Logging;
2016-10-29 05:40:15 +00:00
2017-02-20 20:50:58 +00:00
namespace Emby.Server.Implementations.AppBase
2016-10-29 05:40:15 +00:00
{
/// <summary>
2019-10-25 10:47:20 +00:00
/// Class BaseConfigurationManager.
2016-10-29 05:40:15 +00:00
/// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager
{
2023-10-03 14:25:14 +00:00
private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly object _configurationSyncLock = new();
2021-05-28 12:33:54 +00:00
2019-08-09 21:50:40 +00:00
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
/// <summary>
/// The _configuration.
/// </summary>
2023-02-14 19:09:07 +00:00
private BaseApplicationConfiguration? _configuration;
2019-08-09 21:50:40 +00:00
/// <summary>
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
2023-10-03 14:25:14 +00:00
protected BaseConfigurationManager(
IApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
2019-08-09 21:50:40 +00:00
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
2020-06-06 00:15:56 +00:00
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
2019-08-09 21:50:40 +00:00
UpdateCachePath();
}
2016-10-29 05:40:15 +00:00
/// <summary>
/// Occurs when [configuration updated].
/// </summary>
2023-02-14 19:09:07 +00:00
public event EventHandler<EventArgs>? ConfigurationUpdated;
2016-10-29 05:40:15 +00:00
/// <summary>
/// Occurs when [configuration updating].
/// </summary>
2023-02-14 19:09:07 +00:00
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
2016-10-29 05:40:15 +00:00
/// <summary>
/// Occurs when [named configuration updated].
/// </summary>
2023-02-14 19:09:07 +00:00
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
2016-10-29 05:40:15 +00:00
2019-08-09 21:50:40 +00:00
/// <summary>
/// Gets the type of the configuration.
/// </summary>
/// <value>The type of the configuration.</value>
protected abstract Type ConfigurationType { get; }
2016-10-29 05:40:15 +00:00
/// <summary>
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
2020-06-06 00:15:56 +00:00
protected ILogger<BaseConfigurationManager> Logger { get; private set; }
2016-10-29 05:40:15 +00:00
/// <summary>
/// Gets the XML serializer.
/// </summary>
/// <value>The XML serializer.</value>
protected IXmlSerializer XmlSerializer { get; private set; }
/// <summary>
2019-10-25 10:47:20 +00:00
/// Gets the application paths.
2016-10-29 05:40:15 +00:00
/// </summary>
/// <value>The application paths.</value>
public IApplicationPaths CommonApplicationPaths { get; private set; }
/// <summary>
/// Gets or sets the system configuration.
2016-10-29 05:40:15 +00:00
/// </summary>
/// <value>The configuration.</value>
public BaseApplicationConfiguration CommonConfiguration
{
get
{
2023-02-14 19:09:07 +00:00
if (_configuration is not null)
{
return _configuration;
}
lock (_configurationSyncLock)
{
2023-02-14 19:09:07 +00:00
if (_configuration is not null)
{
return _configuration;
}
2023-02-14 19:09:07 +00:00
return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
}
2016-10-29 05:40:15 +00:00
}
2016-10-29 05:40:15 +00:00
protected set
{
_configuration = value;
}
}
/// <summary>
/// Manually pre-loads a factory so that it is available pre system initialisation.
/// </summary>
/// <typeparam name="T">Class to register.</typeparam>
public virtual void RegisterConfiguration<T>()
where T : IConfigurationFactory
{
IConfigurationFactory factory = Activator.CreateInstance<T>();
2022-12-05 14:00:20 +00:00
if (_configurationFactories is null)
{
2024-04-30 19:32:59 +00:00
_configurationFactories = [factory];
}
else
{
2024-04-30 19:32:59 +00:00
_configurationFactories = [.._configurationFactories, factory];
}
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
2019-11-01 17:38:54 +00:00
/// <summary>
/// Adds parts.
/// </summary>
/// <param name="factories">The configuration factories.</param>
2016-10-29 05:40:15 +00:00
public virtual void AddParts(IEnumerable<IConfigurationFactory> factories)
{
_configurationFactories = factories.ToArray();
_configurationStores = _configurationFactories
.SelectMany(i => i.GetConfigurations())
.ToArray();
}
/// <summary>
/// Saves the configuration.
/// </summary>
public void SaveConfiguration()
{
Logger.LogInformation("Saving system configuration");
2016-10-29 05:40:15 +00:00
var path = CommonApplicationPaths.SystemConfigurationFilePath;
2023-02-14 19:09:07 +00:00
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
2016-10-29 05:40:15 +00:00
lock (_configurationSyncLock)
{
XmlSerializer.SerializeToFile(CommonConfiguration, path);
}
OnConfigurationUpdated();
}
/// <summary>
/// Called when [configuration updated].
/// </summary>
protected virtual void OnConfigurationUpdated()
{
UpdateCachePath();
EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger);
}
/// <summary>
/// Replaces the configuration.
/// </summary>
/// <param name="newConfiguration">The new configuration.</param>
2019-10-25 10:47:20 +00:00
/// <exception cref="ArgumentNullException"><c>newConfiguration</c> is <c>null</c>.</exception>
2016-10-29 05:40:15 +00:00
public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
{
ArgumentNullException.ThrowIfNull(newConfiguration);
2016-10-29 05:40:15 +00:00
ValidateCachePath(newConfiguration);
CommonConfiguration = newConfiguration;
SaveConfiguration();
}
/// <summary>
/// Updates the items by name path.
/// </summary>
private void UpdateCachePath()
{
string cachePath;
2019-08-09 21:50:40 +00:00
2019-02-09 23:37:35 +00:00
// If the configuration file has no entry (i.e. not set in UI)
2016-10-29 05:40:15 +00:00
if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
{
2019-02-09 23:37:35 +00:00
// 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
2019-02-10 00:14:34 +00:00
cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache");
2019-02-09 23:37:35 +00:00
}
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;
}
2016-10-29 05:40:15 +00:00
}
else
{
2019-02-09 23:37:35 +00:00
// Set cachePath to the new UI-set value
cachePath = CommonConfiguration.CachePath;
2016-10-29 05:40:15 +00:00
}
Logger.LogInformation("Setting cache path: {Path}", cachePath);
2016-10-29 05:40:15 +00:00
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
}
/// <summary>
/// Replaces the cache path.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
/// <exception cref="DirectoryNotFoundException">The new cache path doesn't exist.</exception>
2016-10-29 05:40:15 +00:00
private void ValidateCachePath(BaseApplicationConfiguration newConfig)
{
var newPath = newConfig.CachePath;
if (!string.IsNullOrWhiteSpace(newPath)
2019-08-09 21:50:40 +00:00
&& !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal))
2016-10-29 05:40:15 +00:00
{
// Validate
if (!Directory.Exists(newPath))
2016-10-29 05:40:15 +00:00
{
throw new DirectoryNotFoundException(
2019-08-09 21:50:40 +00:00
string.Format(
CultureInfo.InvariantCulture,
"{0} does not exist.",
newPath));
2016-10-29 05:40:15 +00:00
}
EnsureWriteAccess(newPath);
}
}
2019-11-01 17:38:54 +00:00
/// <summary>
/// Ensures that we have write access to the path.
/// </summary>
/// <param name="path">The path.</param>
2016-10-29 05:40:15 +00:00
protected void EnsureWriteAccess(string path)
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
2023-10-03 14:25:14 +00:00
File.Delete(file);
2016-10-29 05:40:15 +00:00
}
private string GetConfigurationFile(string key)
{
2019-01-27 11:03:43 +00:00
return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml");
2016-10-29 05:40:15 +00:00
}
2019-11-01 17:38:54 +00:00
/// <inheritdoc />
2016-10-29 05:40:15 +00:00
public object GetConfiguration(string key)
{
2021-05-23 22:30:41 +00:00
return _configurations.GetOrAdd(
key,
2021-12-19 09:27:57 +00:00
static (k, configurationManager) =>
2021-05-23 22:30:41 +00:00
{
var file = configurationManager.GetConfigurationFile(k);
2016-10-29 05:40:15 +00:00
2021-05-23 22:30:41 +00:00
var configurationInfo = Array.Find(
configurationManager._configurationStores,
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
2016-10-29 05:40:15 +00:00
2022-12-05 14:00:20 +00:00
if (configurationInfo is null)
2021-05-23 22:30:41 +00:00
{
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
}
2016-10-29 05:40:15 +00:00
2021-05-23 22:30:41 +00:00
var configurationType = configurationInfo.ConfigurationType;
2016-10-29 05:40:15 +00:00
2021-05-23 22:30:41 +00:00
lock (configurationManager._configurationSyncLock)
{
return configurationManager.LoadConfiguration(file, configurationType);
}
},
this);
2016-10-29 05:40:15 +00:00
}
private object LoadConfiguration(string path, Type configurationType)
{
try
2016-10-29 05:40:15 +00:00
{
2023-02-14 19:09:07 +00:00
if (File.Exists(path))
{
return XmlSerializer.DeserializeFromFile(configurationType, path);
}
2016-10-29 05:40:15 +00:00
}
2023-02-14 19:09:07 +00:00
catch (Exception ex) when (ex is not IOException)
2016-10-29 05:40:15 +00:00
{
2020-08-31 20:20:19 +00:00
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
2016-10-29 05:40:15 +00:00
}
2023-02-14 19:09:07 +00:00
return Activator.CreateInstance(configurationType)
?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
2016-10-29 05:40:15 +00:00
}
2019-11-01 17:38:54 +00:00
/// <inheritdoc />
2016-10-29 05:40:15 +00:00
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)
2016-10-29 05:40:15 +00:00
{
var currentConfiguration = GetConfiguration(key);
validatingStore.Validate(currentConfiguration, configuration);
}
2022-08-12 18:37:31 +00:00
NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
2016-10-29 05:40:15 +00:00
2021-12-15 17:25:36 +00:00
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
2016-10-29 05:40:15 +00:00
var path = GetConfigurationFile(key);
2023-02-14 19:09:07 +00:00
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
2016-10-29 05:40:15 +00:00
lock (_configurationSyncLock)
{
XmlSerializer.SerializeToFile(configuration, path);
}
OnNamedConfigurationUpdated(key, configuration);
}
2019-11-01 17:38:54 +00:00
/// <summary>
/// Event handler for when a named configuration has been updated.
2019-11-01 17:38:54 +00:00
/// </summary>
/// <param name="key">The key of the configuration.</param>
/// <param name="configuration">The old configuration.</param>
2016-10-29 05:40:15 +00:00
protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
{
2022-08-12 18:37:31 +00:00
NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
2016-10-29 05:40:15 +00:00
}
/// <inheritdoc />
public ConfigurationStore[] GetConfigurationStores()
{
return _configurationStores;
}
2019-11-01 17:38:54 +00:00
/// <inheritdoc />
2016-10-29 05:40:15 +00:00
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));
}
}
}