feat: Implement file-based configuration storage
- Replaced database-dependent ConfigService with pure file-based implementation - Added support for both system-wide (/etc) and local app configuration - Implemented safe atomic file writes with backups - Added fallback to default values for missing configuration fields - Improved error handling for configuration loading/saving 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5d6faef880
commit
c61f308de7
@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -7,84 +9,317 @@ using TransmissionRssManager.Core;
|
|||||||
|
|
||||||
namespace TransmissionRssManager.Services
|
namespace TransmissionRssManager.Services
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for managing application configuration
|
||||||
|
/// File-based implementation that does not use a database
|
||||||
|
/// </summary>
|
||||||
public class ConfigService : IConfigService
|
public class ConfigService : IConfigService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigService> _logger;
|
private readonly ILogger<ConfigService> _logger;
|
||||||
private readonly string _configPath;
|
private readonly string _configFilePath;
|
||||||
private AppConfig _cachedConfig;
|
private AppConfig _cachedConfig;
|
||||||
|
private readonly object _lockObject = new object();
|
||||||
|
|
||||||
public ConfigService(ILogger<ConfigService> logger)
|
public ConfigService(ILogger<ConfigService> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Get config directory
|
// Determine the appropriate config file path
|
||||||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
string baseDir = AppContext.BaseDirectory;
|
||||||
string configDir = Path.Combine(homeDir, ".config", "transmission-rss-manager");
|
string etcConfigPath = "/etc/transmission-rss-manager/appsettings.json";
|
||||||
|
string localConfigPath = Path.Combine(baseDir, "appsettings.json");
|
||||||
|
|
||||||
// Ensure directory exists
|
// Check if config exists in /etc (preferred) or in app directory
|
||||||
if (!Directory.Exists(configDir))
|
_configFilePath = File.Exists(etcConfigPath) ? etcConfigPath : localConfigPath;
|
||||||
{
|
|
||||||
Directory.CreateDirectory(configDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
_configPath = Path.Combine(configDir, "config.json");
|
_logger.LogInformation($"Using configuration file: {_configFilePath}");
|
||||||
_cachedConfig = LoadConfiguration();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AppConfig GetConfiguration()
|
public async Task<AppConfig> GetConfigAsync()
|
||||||
{
|
{
|
||||||
return _cachedConfig;
|
if (_cachedConfig != null)
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveConfigurationAsync(AppConfig config)
|
|
||||||
{
|
|
||||||
_cachedConfig = config;
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
return _cachedConfig;
|
||||||
};
|
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(config, options);
|
|
||||||
await File.WriteAllTextAsync(_configPath, json);
|
|
||||||
|
|
||||||
_logger.LogInformation("Configuration saved successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppConfig LoadConfiguration()
|
|
||||||
{
|
|
||||||
if (!File.Exists(_configPath))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("No configuration file found, creating default");
|
|
||||||
var defaultConfig = CreateDefaultConfig();
|
|
||||||
SaveConfigurationAsync(defaultConfig).Wait();
|
|
||||||
return defaultConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string json = File.ReadAllText(_configPath);
|
_cachedConfig = await LoadConfigFromFileAsync();
|
||||||
var config = JsonSerializer.Deserialize<AppConfig>(json);
|
return _cachedConfig;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error loading configuration, using default values");
|
||||||
|
return CreateDefaultConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveConfigAsync(AppConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cachedConfig = config;
|
||||||
|
await SaveConfigToFileAsync(config);
|
||||||
|
_logger.LogInformation("Configuration saved successfully to file");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving configuration to file");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetSettingAsync(string key, string defaultValue = "")
|
||||||
|
{
|
||||||
|
var config = await GetConfigAsync();
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "Transmission.Host":
|
||||||
|
return config.Transmission.Host ?? defaultValue;
|
||||||
|
case "Transmission.Port":
|
||||||
|
return config.Transmission.Port.ToString();
|
||||||
|
case "Transmission.Username":
|
||||||
|
return config.Transmission.Username ?? defaultValue;
|
||||||
|
case "Transmission.Password":
|
||||||
|
return config.Transmission.Password ?? defaultValue;
|
||||||
|
case "Transmission.UseHttps":
|
||||||
|
return config.Transmission.UseHttps.ToString();
|
||||||
|
case "AutoDownloadEnabled":
|
||||||
|
return config.AutoDownloadEnabled.ToString();
|
||||||
|
case "CheckIntervalMinutes":
|
||||||
|
return config.CheckIntervalMinutes.ToString();
|
||||||
|
case "DownloadDirectory":
|
||||||
|
return config.DownloadDirectory ?? defaultValue;
|
||||||
|
case "MediaLibraryPath":
|
||||||
|
return config.MediaLibraryPath ?? defaultValue;
|
||||||
|
case "PostProcessing.Enabled":
|
||||||
|
return config.PostProcessing.Enabled.ToString();
|
||||||
|
case "PostProcessing.ExtractArchives":
|
||||||
|
return config.PostProcessing.ExtractArchives.ToString();
|
||||||
|
case "PostProcessing.OrganizeMedia":
|
||||||
|
return config.PostProcessing.OrganizeMedia.ToString();
|
||||||
|
case "PostProcessing.MinimumSeedRatio":
|
||||||
|
return config.PostProcessing.MinimumSeedRatio.ToString();
|
||||||
|
case "UserPreferences.EnableDarkMode":
|
||||||
|
return config.UserPreferences.EnableDarkMode.ToString();
|
||||||
|
case "UserPreferences.AutoRefreshUIEnabled":
|
||||||
|
return config.UserPreferences.AutoRefreshUIEnabled.ToString();
|
||||||
|
case "UserPreferences.AutoRefreshIntervalSeconds":
|
||||||
|
return config.UserPreferences.AutoRefreshIntervalSeconds.ToString();
|
||||||
|
case "UserPreferences.NotificationsEnabled":
|
||||||
|
return config.UserPreferences.NotificationsEnabled.ToString();
|
||||||
|
default:
|
||||||
|
_logger.LogWarning($"Unknown setting key: {key}");
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveSettingAsync(string key, string value)
|
||||||
|
{
|
||||||
|
var config = await GetConfigAsync();
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "Transmission.Host":
|
||||||
|
config.Transmission.Host = value;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
case "Transmission.Port":
|
||||||
|
if (int.TryParse(value, out int port))
|
||||||
|
{
|
||||||
|
config.Transmission.Port = port;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Transmission.Username":
|
||||||
|
config.Transmission.Username = value;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
case "Transmission.Password":
|
||||||
|
config.Transmission.Password = value;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
case "Transmission.UseHttps":
|
||||||
|
if (bool.TryParse(value, out bool useHttps))
|
||||||
|
{
|
||||||
|
config.Transmission.UseHttps = useHttps;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "AutoDownloadEnabled":
|
||||||
|
if (bool.TryParse(value, out bool autoDownload))
|
||||||
|
{
|
||||||
|
config.AutoDownloadEnabled = autoDownload;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "CheckIntervalMinutes":
|
||||||
|
if (int.TryParse(value, out int interval))
|
||||||
|
{
|
||||||
|
config.CheckIntervalMinutes = interval;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "DownloadDirectory":
|
||||||
|
config.DownloadDirectory = value;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
case "MediaLibraryPath":
|
||||||
|
config.MediaLibraryPath = value;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
case "PostProcessing.Enabled":
|
||||||
|
if (bool.TryParse(value, out bool ppEnabled))
|
||||||
|
{
|
||||||
|
config.PostProcessing.Enabled = ppEnabled;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PostProcessing.ExtractArchives":
|
||||||
|
if (bool.TryParse(value, out bool extractArchives))
|
||||||
|
{
|
||||||
|
config.PostProcessing.ExtractArchives = extractArchives;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PostProcessing.OrganizeMedia":
|
||||||
|
if (bool.TryParse(value, out bool organizeMedia))
|
||||||
|
{
|
||||||
|
config.PostProcessing.OrganizeMedia = organizeMedia;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PostProcessing.MinimumSeedRatio":
|
||||||
|
if (float.TryParse(value, out float seedRatio))
|
||||||
|
{
|
||||||
|
config.PostProcessing.MinimumSeedRatio = seedRatio;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "UserPreferences.EnableDarkMode":
|
||||||
|
if (bool.TryParse(value, out bool darkMode))
|
||||||
|
{
|
||||||
|
config.UserPreferences.EnableDarkMode = darkMode;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "UserPreferences.AutoRefreshUIEnabled":
|
||||||
|
if (bool.TryParse(value, out bool autoRefresh))
|
||||||
|
{
|
||||||
|
config.UserPreferences.AutoRefreshUIEnabled = autoRefresh;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "UserPreferences.AutoRefreshIntervalSeconds":
|
||||||
|
if (int.TryParse(value, out int refreshInterval))
|
||||||
|
{
|
||||||
|
config.UserPreferences.AutoRefreshIntervalSeconds = refreshInterval;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "UserPreferences.NotificationsEnabled":
|
||||||
|
if (bool.TryParse(value, out bool notifications))
|
||||||
|
{
|
||||||
|
config.UserPreferences.NotificationsEnabled = notifications;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_logger.LogWarning($"Unknown setting key: {key}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
await SaveConfigAsync(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error saving setting {key}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AppConfig> LoadConfigFromFileAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_configFilePath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Configuration file not found at {_configFilePath}, creating default config");
|
||||||
|
var defaultConfig = CreateDefaultConfig();
|
||||||
|
await SaveConfigToFileAsync(defaultConfig);
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = await File.ReadAllTextAsync(_configFilePath);
|
||||||
|
var config = JsonSerializer.Deserialize<AppConfig>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Failed to deserialize config, creating default");
|
throw new InvalidOperationException("Failed to deserialize configuration");
|
||||||
return CreateDefaultConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in any missing values with defaults
|
||||||
|
EnsureCompleteConfig(config);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error loading configuration");
|
_logger.LogError(ex, "Error loading configuration from file");
|
||||||
return CreateDefaultConfig();
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveConfigToFileAsync(AppConfig config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = JsonSerializer.Serialize(config, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
string directory = Path.GetDirectoryName(_configFilePath);
|
||||||
|
if (!Directory.Exists(directory) && !string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a temporary file to avoid corruption if the process crashes during write
|
||||||
|
string tempFilePath = _configFilePath + ".tmp";
|
||||||
|
await File.WriteAllTextAsync(tempFilePath, json);
|
||||||
|
|
||||||
|
// Atomic file replacement (as much as the filesystem allows)
|
||||||
|
if (File.Exists(_configFilePath))
|
||||||
|
{
|
||||||
|
File.Replace(tempFilePath, _configFilePath, _configFilePath + ".bak");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(tempFilePath, _configFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error saving configuration to file");
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppConfig CreateDefaultConfig()
|
private AppConfig CreateDefaultConfig()
|
||||||
{
|
{
|
||||||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
||||||
|
|
||||||
return new AppConfig
|
return new AppConfig
|
||||||
{
|
{
|
||||||
Transmission = new TransmissionConfig
|
Transmission = new TransmissionConfig
|
||||||
@ -92,21 +327,64 @@ namespace TransmissionRssManager.Services
|
|||||||
Host = "localhost",
|
Host = "localhost",
|
||||||
Port = 9091,
|
Port = 9091,
|
||||||
Username = "",
|
Username = "",
|
||||||
Password = ""
|
Password = "",
|
||||||
|
UseHttps = false
|
||||||
},
|
},
|
||||||
AutoDownloadEnabled = false,
|
AutoDownloadEnabled = true,
|
||||||
CheckIntervalMinutes = 30,
|
CheckIntervalMinutes = 30,
|
||||||
DownloadDirectory = Path.Combine(homeDir, "Downloads"),
|
DownloadDirectory = "/var/lib/transmission-daemon/downloads",
|
||||||
MediaLibraryPath = Path.Combine(homeDir, "Media"),
|
MediaLibraryPath = "/media/library",
|
||||||
PostProcessing = new PostProcessingConfig
|
PostProcessing = new PostProcessingConfig
|
||||||
{
|
{
|
||||||
Enabled = false,
|
Enabled = false,
|
||||||
ExtractArchives = true,
|
ExtractArchives = true,
|
||||||
OrganizeMedia = true,
|
OrganizeMedia = true,
|
||||||
MinimumSeedRatio = 1,
|
MinimumSeedRatio = 1.0f
|
||||||
MediaExtensions = new System.Collections.Generic.List<string> { ".mp4", ".mkv", ".avi" }
|
},
|
||||||
|
UserPreferences = new UserPreferencesConfig
|
||||||
|
{
|
||||||
|
EnableDarkMode = true,
|
||||||
|
AutoRefreshUIEnabled = true,
|
||||||
|
AutoRefreshIntervalSeconds = 30,
|
||||||
|
NotificationsEnabled = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureCompleteConfig(AppConfig config)
|
||||||
|
{
|
||||||
|
// Create new instances for any null nested objects
|
||||||
|
config.Transmission ??= new TransmissionConfig
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = 9091,
|
||||||
|
Username = "",
|
||||||
|
Password = "",
|
||||||
|
UseHttps = false
|
||||||
|
};
|
||||||
|
|
||||||
|
config.PostProcessing ??= new PostProcessingConfig
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
ExtractArchives = true,
|
||||||
|
OrganizeMedia = true,
|
||||||
|
MinimumSeedRatio = 1.0f
|
||||||
|
};
|
||||||
|
|
||||||
|
config.UserPreferences ??= new UserPreferencesConfig
|
||||||
|
{
|
||||||
|
EnableDarkMode = true,
|
||||||
|
AutoRefreshUIEnabled = true,
|
||||||
|
AutoRefreshIntervalSeconds = 30,
|
||||||
|
NotificationsEnabled = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure default values for string properties if they're null
|
||||||
|
config.DownloadDirectory ??= "/var/lib/transmission-daemon/downloads";
|
||||||
|
config.MediaLibraryPath ??= "/media/library";
|
||||||
|
config.Transmission.Host ??= "localhost";
|
||||||
|
config.Transmission.Username ??= "";
|
||||||
|
config.Transmission.Password ??= "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user