mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-02-23 01:56:22 -05:00
This commit is contained in:
309
code/Executable/Controllers/ConfigFilesController.cs
Normal file
309
code/Executable/Controllers/ConfigFilesController.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using Infrastructure.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Executable.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/config-files")]
|
||||
public class ConfigFilesController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ConfigFilesController> _logger;
|
||||
private readonly IConfigurationManager _configManager;
|
||||
|
||||
public ConfigFilesController(
|
||||
ILogger<ConfigFilesController> logger,
|
||||
IConfigurationManager configManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_configManager = configManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all available configuration files
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public IActionResult GetAllConfigFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
var files = _configManager.ListConfigurationFiles().ToList();
|
||||
return Ok(new { Files = files, Count = files.Count });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error listing configuration files");
|
||||
return StatusCode(500, "An error occurred while listing configuration files");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content of a specific configuration file
|
||||
/// </summary>
|
||||
[HttpGet("{fileName}")]
|
||||
public async Task<IActionResult> GetConfigFile(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".json"))
|
||||
{
|
||||
fileName = $"{fileName}.json";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read as dynamic to support any JSON structure
|
||||
var config = await _configManager.GetConfigurationAsync<object>(fileName);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
return NotFound($"Configuration file '{fileName}' not found");
|
||||
}
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading configuration file {fileName}", fileName);
|
||||
return StatusCode(500, $"An error occurred while reading configuration file '{fileName}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates a configuration file
|
||||
/// </summary>
|
||||
[HttpPut("{fileName}")]
|
||||
public async Task<IActionResult> SaveConfigFile(string fileName, [FromBody] JsonElement content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".json"))
|
||||
{
|
||||
fileName = $"{fileName}.json";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Convert the JsonElement to an object
|
||||
var configObject = JsonSerializer.Deserialize<object>(content.GetRawText());
|
||||
|
||||
if (configObject == null)
|
||||
{
|
||||
return BadRequest("Invalid JSON content");
|
||||
}
|
||||
|
||||
var result = await _configManager.SaveConfigurationAsync(fileName, configObject);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return StatusCode(500, $"Failed to save configuration file '{fileName}'");
|
||||
}
|
||||
|
||||
return Ok(new { Message = $"Configuration file '{fileName}' saved successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving configuration file {fileName}", fileName);
|
||||
return StatusCode(500, $"An error occurred while saving configuration file '{fileName}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a configuration file
|
||||
/// </summary>
|
||||
[HttpDelete("{fileName}")]
|
||||
public async Task<IActionResult> DeleteConfigFile(string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".json"))
|
||||
{
|
||||
fileName = $"{fileName}.json";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _configManager.DeleteConfigurationAsync(fileName);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return StatusCode(500, $"Failed to delete configuration file '{fileName}'");
|
||||
}
|
||||
|
||||
return Ok(new { Message = $"Configuration file '{fileName}' deleted successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting configuration file {fileName}", fileName);
|
||||
return StatusCode(500, $"An error occurred while deleting configuration file '{fileName}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a specific property in a configuration file
|
||||
/// </summary>
|
||||
[HttpPatch("{fileName}")]
|
||||
public async Task<IActionResult> UpdateConfigProperty(
|
||||
string fileName,
|
||||
[FromQuery] string propertyPath,
|
||||
[FromBody] JsonElement value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".json"))
|
||||
{
|
||||
fileName = $"{fileName}.json";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(propertyPath))
|
||||
{
|
||||
return BadRequest("Property path is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Convert the JsonElement to an object
|
||||
var valueObject = JsonSerializer.Deserialize<object>(value.GetRawText());
|
||||
|
||||
if (valueObject == null)
|
||||
{
|
||||
return BadRequest("Invalid value");
|
||||
}
|
||||
|
||||
var result = await _configManager.UpdateConfigurationPropertyAsync(fileName, propertyPath, valueObject);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return StatusCode(500, $"Failed to update property '{propertyPath}' in configuration file '{fileName}'");
|
||||
}
|
||||
|
||||
return Ok(new { Message = $"Property '{propertyPath}' in configuration file '{fileName}' updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating property {propertyPath} in configuration file {fileName}", propertyPath, fileName);
|
||||
return StatusCode(500, $"An error occurred while updating property '{propertyPath}' in configuration file '{fileName}'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Sonarr configuration
|
||||
/// </summary>
|
||||
[HttpGet("sonarr")]
|
||||
public async Task<IActionResult> GetSonarrConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetSonarrConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting Sonarr configuration");
|
||||
return StatusCode(500, "An error occurred while getting Sonarr configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Radarr configuration
|
||||
/// </summary>
|
||||
[HttpGet("radarr")]
|
||||
public async Task<IActionResult> GetRadarrConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetRadarrConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting Radarr configuration");
|
||||
return StatusCode(500, "An error occurred while getting Radarr configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Lidarr configuration
|
||||
/// </summary>
|
||||
[HttpGet("lidarr")]
|
||||
public async Task<IActionResult> GetLidarrConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetLidarrConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting Lidarr configuration");
|
||||
return StatusCode(500, "An error occurred while getting Lidarr configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ContentBlocker configuration
|
||||
/// </summary>
|
||||
[HttpGet("contentblocker")]
|
||||
public async Task<IActionResult> GetContentBlockerConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetContentBlockerConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting ContentBlocker configuration");
|
||||
return StatusCode(500, "An error occurred while getting ContentBlocker configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the QueueCleaner configuration
|
||||
/// </summary>
|
||||
[HttpGet("queuecleaner")]
|
||||
public async Task<IActionResult> GetQueueCleanerConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetQueueCleanerConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting QueueCleaner configuration");
|
||||
return StatusCode(500, "An error occurred while getting QueueCleaner configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DownloadCleaner configuration
|
||||
/// </summary>
|
||||
[HttpGet("downloadcleaner")]
|
||||
public async Task<IActionResult> GetDownloadCleanerConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetDownloadCleanerConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting DownloadCleaner configuration");
|
||||
return StatusCode(500, "An error occurred while getting DownloadCleaner configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DownloadClient configuration
|
||||
/// </summary>
|
||||
[HttpGet("downloadclient")]
|
||||
public async Task<IActionResult> GetDownloadClientConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _configManager.GetDownloadClientConfigAsync();
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting DownloadClient configuration");
|
||||
return StatusCode(500, "An error occurred while getting DownloadClient configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
using Common.Configuration.Arr;
|
||||
using Common.Configuration.Arr;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Common.Configuration.DownloadCleaner;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.General;
|
||||
using Common.Configuration.Logging;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Infrastructure.Configuration;
|
||||
using Infrastructure.Services;
|
||||
using System.IO;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class ConfigurationDI
|
||||
{
|
||||
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Configure options from appsettings.json
|
||||
services
|
||||
.Configure<DryRunConfig>(configuration)
|
||||
.Configure<SearchConfig>(configuration)
|
||||
@@ -25,4 +30,14 @@ public static class ConfigurationDI
|
||||
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName))
|
||||
.Configure<LidarrConfig>(configuration.GetSection(LidarrConfig.SectionName))
|
||||
.Configure<LoggingConfig>(configuration.GetSection(LoggingConfig.SectionName));
|
||||
|
||||
// Add JSON-based configuration services
|
||||
string configDirectory = Path.Combine(
|
||||
Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) ?? AppDomain.CurrentDomain.BaseDirectory,
|
||||
"config");
|
||||
|
||||
services.AddConfigurationServices(configDirectory);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
20
code/Infrastructure/Configuration/ConfigurationExtensions.cs
Normal file
20
code/Infrastructure/Configuration/ConfigurationExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static IServiceCollection AddConfigurationServices(this IServiceCollection services, string configDirectory)
|
||||
{
|
||||
services.AddSingleton<JsonConfigurationProvider>(provider =>
|
||||
{
|
||||
var logger = provider.GetRequiredService<ILogger<JsonConfigurationProvider>>();
|
||||
return new JsonConfigurationProvider(logger, configDirectory);
|
||||
});
|
||||
|
||||
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
271
code/Infrastructure/Configuration/ConfigurationManager.cs
Normal file
271
code/Infrastructure/Configuration/ConfigurationManager.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.Arr;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Common.Configuration.DownloadCleaner;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides configuration management for various components with thread-safe file access.
|
||||
/// </summary>
|
||||
public interface IConfigurationManager
|
||||
{
|
||||
// Configuration files
|
||||
Task<T?> GetConfigurationAsync<T>(string configFileName) where T : class, new();
|
||||
Task<bool> SaveConfigurationAsync<T>(string configFileName, T config) where T : class;
|
||||
Task<bool> UpdateConfigurationPropertyAsync<T>(string configFileName, string propertyPath, T value);
|
||||
Task<bool> MergeConfigurationAsync<T>(string configFileName, T newValues) where T : class;
|
||||
Task<bool> DeleteConfigurationAsync(string configFileName);
|
||||
IEnumerable<string> ListConfigurationFiles();
|
||||
|
||||
// Specific configuration types (for common configs)
|
||||
Task<SonarrConfig?> GetSonarrConfigAsync();
|
||||
Task<RadarrConfig?> GetRadarrConfigAsync();
|
||||
Task<LidarrConfig?> GetLidarrConfigAsync();
|
||||
Task<ContentBlockerConfig?> GetContentBlockerConfigAsync();
|
||||
Task<QueueCleanerConfig?> GetQueueCleanerConfigAsync();
|
||||
Task<DownloadCleanerConfig?> GetDownloadCleanerConfigAsync();
|
||||
Task<DownloadClientConfig?> GetDownloadClientConfigAsync();
|
||||
|
||||
Task<bool> SaveSonarrConfigAsync(SonarrConfig config);
|
||||
Task<bool> SaveRadarrConfigAsync(RadarrConfig config);
|
||||
Task<bool> SaveLidarrConfigAsync(LidarrConfig config);
|
||||
Task<bool> SaveContentBlockerConfigAsync(ContentBlockerConfig config);
|
||||
Task<bool> SaveQueueCleanerConfigAsync(QueueCleanerConfig config);
|
||||
Task<bool> SaveDownloadCleanerConfigAsync(DownloadCleanerConfig config);
|
||||
Task<bool> SaveDownloadClientConfigAsync(DownloadClientConfig config);
|
||||
}
|
||||
|
||||
public class ConfigurationManager : IConfigurationManager
|
||||
{
|
||||
private readonly ILogger<ConfigurationManager> _logger;
|
||||
private readonly JsonConfigurationProvider _configProvider;
|
||||
private readonly IOptionsMonitor<SonarrConfig> _sonarrConfig;
|
||||
private readonly IOptionsMonitor<RadarrConfig> _radarrConfig;
|
||||
private readonly IOptionsMonitor<LidarrConfig> _lidarrConfig;
|
||||
private readonly IOptionsMonitor<ContentBlockerConfig> _contentBlockerConfig;
|
||||
private readonly IOptionsMonitor<QueueCleanerConfig> _queueCleanerConfig;
|
||||
private readonly IOptionsMonitor<DownloadCleanerConfig> _downloadCleanerConfig;
|
||||
private readonly IOptionsMonitor<DownloadClientConfig> _downloadClientConfig;
|
||||
|
||||
// Define standard config file names
|
||||
private const string SonarrConfigFile = "sonarr.json";
|
||||
private const string RadarrConfigFile = "radarr.json";
|
||||
private const string LidarrConfigFile = "lidarr.json";
|
||||
private const string ContentBlockerConfigFile = "contentblocker.json";
|
||||
private const string QueueCleanerConfigFile = "queuecleaner.json";
|
||||
private const string DownloadCleanerConfigFile = "downloadcleaner.json";
|
||||
private const string DownloadClientConfigFile = "downloadclient.json";
|
||||
|
||||
public ConfigurationManager(
|
||||
ILogger<ConfigurationManager> logger,
|
||||
JsonConfigurationProvider configProvider,
|
||||
IOptionsMonitor<SonarrConfig> sonarrConfig,
|
||||
IOptionsMonitor<RadarrConfig> radarrConfig,
|
||||
IOptionsMonitor<LidarrConfig> lidarrConfig,
|
||||
IOptionsMonitor<ContentBlockerConfig> contentBlockerConfig,
|
||||
IOptionsMonitor<QueueCleanerConfig> queueCleanerConfig,
|
||||
IOptionsMonitor<DownloadCleanerConfig> downloadCleanerConfig,
|
||||
IOptionsMonitor<DownloadClientConfig> downloadClientConfig)
|
||||
{
|
||||
_logger = logger;
|
||||
_configProvider = configProvider;
|
||||
_sonarrConfig = sonarrConfig;
|
||||
_radarrConfig = radarrConfig;
|
||||
_lidarrConfig = lidarrConfig;
|
||||
_contentBlockerConfig = contentBlockerConfig;
|
||||
_queueCleanerConfig = queueCleanerConfig;
|
||||
_downloadCleanerConfig = downloadCleanerConfig;
|
||||
_downloadClientConfig = downloadClientConfig;
|
||||
}
|
||||
|
||||
// Generic configuration methods
|
||||
public Task<T?> GetConfigurationAsync<T>(string configFileName) where T : class, new()
|
||||
{
|
||||
return _configProvider.ReadConfigurationAsync<T>(configFileName);
|
||||
}
|
||||
|
||||
public Task<bool> SaveConfigurationAsync<T>(string configFileName, T config) where T : class
|
||||
{
|
||||
// Validate if it's an IConfig
|
||||
if (config is IConfig configurable)
|
||||
{
|
||||
try
|
||||
{
|
||||
configurable.Validate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Configuration validation failed for {fileName}", configFileName);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
return _configProvider.WriteConfigurationAsync(configFileName, config);
|
||||
}
|
||||
|
||||
public Task<bool> UpdateConfigurationPropertyAsync<T>(string configFileName, string propertyPath, T value)
|
||||
{
|
||||
return _configProvider.UpdateConfigurationPropertyAsync(configFileName, propertyPath, value);
|
||||
}
|
||||
|
||||
public Task<bool> MergeConfigurationAsync<T>(string configFileName, T newValues) where T : class
|
||||
{
|
||||
return _configProvider.MergeConfigurationAsync(configFileName, newValues);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteConfigurationAsync(string configFileName)
|
||||
{
|
||||
return _configProvider.DeleteConfigurationAsync(configFileName);
|
||||
}
|
||||
|
||||
public IEnumerable<string> ListConfigurationFiles()
|
||||
{
|
||||
return _configProvider.ListConfigurationFiles();
|
||||
}
|
||||
|
||||
// Specific configuration type methods
|
||||
public async Task<SonarrConfig?> GetSonarrConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<SonarrConfig>(SonarrConfigFile);
|
||||
return config ?? _sonarrConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<RadarrConfig?> GetRadarrConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<RadarrConfig>(RadarrConfigFile);
|
||||
return config ?? _radarrConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<LidarrConfig?> GetLidarrConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<LidarrConfig>(LidarrConfigFile);
|
||||
return config ?? _lidarrConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<ContentBlockerConfig?> GetContentBlockerConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<ContentBlockerConfig>(ContentBlockerConfigFile);
|
||||
return config ?? _contentBlockerConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<QueueCleanerConfig?> GetQueueCleanerConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<QueueCleanerConfig>(QueueCleanerConfigFile);
|
||||
return config ?? _queueCleanerConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<DownloadCleanerConfig?> GetDownloadCleanerConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<DownloadCleanerConfig>(DownloadCleanerConfigFile);
|
||||
return config ?? _downloadCleanerConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public async Task<DownloadClientConfig?> GetDownloadClientConfigAsync()
|
||||
{
|
||||
var config = await _configProvider.ReadConfigurationAsync<DownloadClientConfig>(DownloadClientConfigFile);
|
||||
return config ?? _downloadClientConfig.CurrentValue;
|
||||
}
|
||||
|
||||
public Task<bool> SaveSonarrConfigAsync(SonarrConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(SonarrConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Sonarr configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveRadarrConfigAsync(RadarrConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(RadarrConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Radarr configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveLidarrConfigAsync(LidarrConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(LidarrConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Lidarr configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveContentBlockerConfigAsync(ContentBlockerConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(ContentBlockerConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "ContentBlocker configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveQueueCleanerConfigAsync(QueueCleanerConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(QueueCleanerConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "QueueCleaner configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveDownloadCleanerConfigAsync(DownloadCleanerConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(DownloadCleanerConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DownloadCleaner configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> SaveDownloadClientConfigAsync(DownloadClientConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config.Validate();
|
||||
return _configProvider.WriteConfigurationAsync(DownloadClientConfigFile, config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DownloadClient configuration validation failed");
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
390
code/Infrastructure/Configuration/JsonConfigurationProvider.cs
Normal file
390
code/Infrastructure/Configuration/JsonConfigurationProvider.cs
Normal file
@@ -0,0 +1,390 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Provides thread-safe access to JSON configuration files.
|
||||
/// </summary>
|
||||
public class JsonConfigurationProvider
|
||||
{
|
||||
private readonly ILogger<JsonConfigurationProvider> _logger;
|
||||
private readonly string _configDirectory;
|
||||
private readonly Dictionary<string, SemaphoreSlim> _fileLocks = new();
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public JsonConfigurationProvider(ILogger<JsonConfigurationProvider> logger, string configDirectory)
|
||||
{
|
||||
_logger = logger;
|
||||
_configDirectory = configDirectory;
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!Directory.Exists(_configDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_configDirectory);
|
||||
_logger.LogInformation("Created configuration directory: {directory}", _configDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create configuration directory: {directory}", _configDirectory);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock object for a specific file, creating it if necessary.
|
||||
/// </summary>
|
||||
private SemaphoreSlim GetFileLock(string fileName)
|
||||
{
|
||||
if (_fileLocks.TryGetValue(fileName, out var semaphore))
|
||||
{
|
||||
return semaphore;
|
||||
}
|
||||
|
||||
semaphore = new SemaphoreSlim(1, 1);
|
||||
_fileLocks[fileName] = semaphore;
|
||||
return semaphore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path to a configuration file.
|
||||
/// </summary>
|
||||
private string GetFullPath(string fileName)
|
||||
{
|
||||
return Path.Combine(_configDirectory, fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a configuration from a JSON file.
|
||||
/// </summary>
|
||||
public async Task<T?> ReadConfigurationAsync<T>(string fileName) where T : class, new()
|
||||
{
|
||||
var fileLock = GetFileLock(fileName);
|
||||
var fullPath = GetFullPath(fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await fileLock.WaitAsync();
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogWarning("Configuration file does not exist: {file}", fullPath);
|
||||
return new T();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(fullPath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
_logger.LogWarning("Configuration file is empty: {file}", fullPath);
|
||||
return new T();
|
||||
}
|
||||
|
||||
var config = JsonSerializer.Deserialize<T>(json, _serializerOptions);
|
||||
_logger.LogDebug("Read configuration from {file}", fullPath);
|
||||
return config ?? new T();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading configuration from {file}", fullPath);
|
||||
return new T();
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a configuration to a JSON file in a thread-safe manner.
|
||||
/// </summary>
|
||||
public async Task<bool> WriteConfigurationAsync<T>(string fileName, T configuration) where T : class
|
||||
{
|
||||
var fileLock = GetFileLock(fileName);
|
||||
var fullPath = GetFullPath(fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await fileLock.WaitAsync();
|
||||
|
||||
// Create backup if file exists
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
var backupPath = $"{fullPath}.bak";
|
||||
try
|
||||
{
|
||||
File.Copy(fullPath, backupPath, true);
|
||||
_logger.LogDebug("Created backup of configuration file: {backup}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create backup of configuration file: {file}", fullPath);
|
||||
// Continue anyway - prefer having new config to having no config
|
||||
}
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(configuration, _serializerOptions);
|
||||
await File.WriteAllTextAsync(fullPath, json);
|
||||
|
||||
_logger.LogInformation("Wrote configuration to {file}", fullPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error writing configuration to {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a specific property within a JSON configuration file.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateConfigurationPropertyAsync<T>(string fileName, string propertyPath, T value)
|
||||
{
|
||||
var fileLock = GetFileLock(fileName);
|
||||
var fullPath = GetFullPath(fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await fileLock.WaitAsync();
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogWarning("Configuration file does not exist: {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create backup
|
||||
var backupPath = $"{fullPath}.bak";
|
||||
try
|
||||
{
|
||||
File.Copy(fullPath, backupPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create backup of configuration file: {file}", fullPath);
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(fullPath);
|
||||
var jsonNode = JsonNode.Parse(json)?.AsObject();
|
||||
|
||||
if (jsonNode == null)
|
||||
{
|
||||
_logger.LogError("Failed to parse configuration file: {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle simple property paths like "propertyName"
|
||||
if (!propertyPath.Contains('.'))
|
||||
{
|
||||
jsonNode[propertyPath] = JsonValue.Create(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle nested property paths like "parent.child.property"
|
||||
var parts = propertyPath.Split('.');
|
||||
var current = jsonNode;
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
if (current[parts[i]] is JsonObject nestedObject)
|
||||
{
|
||||
current = nestedObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newObject = new JsonObject();
|
||||
current[parts[i]] = newObject;
|
||||
current = newObject;
|
||||
}
|
||||
}
|
||||
|
||||
current[parts[^1]] = JsonValue.Create(value);
|
||||
}
|
||||
|
||||
var updatedJson = jsonNode.ToJsonString(_serializerOptions);
|
||||
await File.WriteAllTextAsync(fullPath, updatedJson);
|
||||
|
||||
_logger.LogInformation("Updated property {property} in {file}", propertyPath, fullPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating property {property} in {file}", propertyPath, fullPath);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges an existing configuration with new values.
|
||||
/// </summary>
|
||||
public async Task<bool> MergeConfigurationAsync<T>(string fileName, T newValues) where T : class
|
||||
{
|
||||
var fileLock = GetFileLock(fileName);
|
||||
var fullPath = GetFullPath(fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await fileLock.WaitAsync();
|
||||
|
||||
T currentConfig;
|
||||
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(fullPath);
|
||||
currentConfig = JsonSerializer.Deserialize<T>(json, _serializerOptions) ?? Activator.CreateInstance<T>();
|
||||
|
||||
// Create backup
|
||||
var backupPath = $"{fullPath}.bak";
|
||||
try
|
||||
{
|
||||
File.Copy(fullPath, backupPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create backup of configuration file: {file}", fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentConfig = Activator.CreateInstance<T>() ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}");
|
||||
}
|
||||
|
||||
// Merge properties using JsonNode
|
||||
var currentJson = JsonSerializer.Serialize(currentConfig, _serializerOptions);
|
||||
var currentNode = JsonNode.Parse(currentJson)?.AsObject();
|
||||
|
||||
var newJson = JsonSerializer.Serialize(newValues, _serializerOptions);
|
||||
var newNode = JsonNode.Parse(newJson)?.AsObject();
|
||||
|
||||
if (currentNode == null || newNode == null)
|
||||
{
|
||||
_logger.LogError("Failed to parse configuration for merging: {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
MergeJsonNodes(currentNode, newNode);
|
||||
|
||||
var mergedJson = currentNode.ToJsonString(_serializerOptions);
|
||||
await File.WriteAllTextAsync(fullPath, mergedJson);
|
||||
|
||||
_logger.LogInformation("Merged configuration in {file}", fullPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error merging configuration in {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void MergeJsonNodes(JsonObject target, JsonObject source)
|
||||
{
|
||||
foreach (var property in source)
|
||||
{
|
||||
if (property.Value is JsonObject sourceObject)
|
||||
{
|
||||
if (target[property.Key] is JsonObject targetObject)
|
||||
{
|
||||
// Recursively merge nested objects
|
||||
MergeJsonNodes(targetObject, sourceObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Replace with new object
|
||||
target[property.Key] = sourceObject.DeepClone();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Replace value
|
||||
target[property.Key] = property.Value?.DeepClone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a configuration file.
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteConfigurationAsync(string fileName)
|
||||
{
|
||||
var fileLock = GetFileLock(fileName);
|
||||
var fullPath = GetFullPath(fileName);
|
||||
|
||||
try
|
||||
{
|
||||
await fileLock.WaitAsync();
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
_logger.LogWarning("Configuration file does not exist: {file}", fullPath);
|
||||
return true; // Already gone
|
||||
}
|
||||
|
||||
// Create backup
|
||||
var backupPath = $"{fullPath}.bak";
|
||||
try
|
||||
{
|
||||
File.Copy(fullPath, backupPath, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create backup of configuration file: {file}", fullPath);
|
||||
}
|
||||
|
||||
File.Delete(fullPath);
|
||||
_logger.LogInformation("Deleted configuration file: {file}", fullPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting configuration file: {file}", fullPath);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all configuration files in the configuration directory.
|
||||
/// </summary>
|
||||
public IEnumerable<string> ListConfigurationFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(_configDirectory, "*.json")
|
||||
.Select(Path.GetFileName)
|
||||
.Where(f => !f.EndsWith(".bak"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error listing configuration files in {directory}", _configDirectory);
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user