This commit is contained in:
Flaminel
2025-05-14 22:49:39 +03:00
parent 69788d55d2
commit 0fc7352db6
5 changed files with 1007 additions and 2 deletions

View 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");
}
}
}

View File

@@ -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;
}
}

View 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;
}
}

View 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);
}
}
}

View 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>();
}
}
}