//! Configuration persistence - loading and saving settings. use std::fs; use std::io::{self, Read, Write}; use std::path::PathBuf; use tracing::{debug, info, warn}; use super::{get_config_dir, Settings, CONFIG_FILE_NAME}; use crate::error::{ConfigError, Result}; /// Configuration persistence handler. pub struct ConfigPersistence { config_path: PathBuf, } impl ConfigPersistence { /// Create a new persistence handler with the given config path. pub fn new(config_path: PathBuf) -> Self { Self { config_path } } /// Create a persistence handler using the default config location. pub fn default_location() -> io::Result { let config_dir = get_config_dir() .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Config directory not found"))?; Ok(Self::new(config_dir.join(CONFIG_FILE_NAME))) } /// Get the configuration file path. pub fn config_path(&self) -> &PathBuf { &self.config_path } /// Check if the configuration file exists. pub fn exists(&self) -> bool { self.config_path.exists() } /// Load settings from the configuration file. pub fn load(&self) -> Result { if !self.exists() { debug!("Config file not found, using defaults"); return Ok(Settings::default()); } let mut file = fs::File::open(&self.config_path).map_err(ConfigError::ReadError)?; let mut contents = String::new(); file.read_to_string(&mut contents) .map_err(ConfigError::ReadError)?; let settings: Settings = toml::from_str(&contents).map_err(ConfigError::from)?; info!("Loaded configuration from {:?}", self.config_path); Ok(settings) } /// Save settings to the configuration file. pub fn save(&self, settings: &Settings) -> Result<()> { // Ensure parent directory exists if let Some(parent) = self.config_path.parent() { if !parent.exists() { fs::create_dir_all(parent).map_err(ConfigError::WriteError)?; debug!("Created config directory: {:?}", parent); } } let contents = toml::to_string_pretty(settings) .map_err(|e| ConfigError::InvalidConfig(e.to_string()))?; let mut file = fs::File::create(&self.config_path).map_err(ConfigError::WriteError)?; file.write_all(contents.as_bytes()) .map_err(ConfigError::WriteError)?; info!("Saved configuration to {:?}", self.config_path); Ok(()) } /// Validate settings and apply any necessary migrations. pub fn validate_and_migrate(&self, settings: &mut Settings) -> Result<()> { // Ensure output directory exists if !settings.output.path.exists() { fs::create_dir_all(&settings.output.path).map_err(|e| { ConfigError::InvalidConfig(format!("Cannot create output directory: {}", e)) })?; debug!("Created output directory: {:?}", settings.output.path); } // Validate frame rate if settings.video.frame_rate == 0 || settings.video.frame_rate > 240 { warn!( "Invalid frame rate {}, defaulting to 60", settings.video.frame_rate ); settings.video.frame_rate = 60; } // Validate container format let valid_containers = ["mp4", "mkv", "mov", "flv"]; if !valid_containers.contains(&settings.output.container.as_str()) { warn!( "Invalid container format '{}', defaulting to mp4", settings.output.container ); settings.output.container = "mp4".to_string(); } // Validate log level let valid_levels = ["trace", "debug", "info", "warn", "error"]; if !valid_levels.contains(&settings.daemon.log_level.as_str()) { warn!( "Invalid log level '{}', defaulting to info", settings.daemon.log_level ); settings.daemon.log_level = "info".to_string(); } Ok(()) } } /// Load configuration from the default location. pub fn load_config() -> Result { let persistence = ConfigPersistence::default_location().map_err(|e| { ConfigError::InvalidConfig(format!("Cannot access config directory: {}", e)) })?; let mut settings = persistence.load()?; persistence.validate_and_migrate(&mut settings)?; Ok(settings) } /// Save configuration to the default location. pub fn save_config(settings: &Settings) -> Result<()> { let persistence = ConfigPersistence::default_location().map_err(|e| { ConfigError::InvalidConfig(format!("Cannot access config directory: {}", e)) })?; persistence.save(settings) } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; #[test] fn test_save_and_load_config() { let dir = tempdir().unwrap(); let config_path = dir.path().join("config.toml"); let persistence = ConfigPersistence::new(config_path); let settings = Settings::default(); persistence.save(&settings).unwrap(); assert!(persistence.exists()); let loaded = persistence.load().unwrap(); assert_eq!(settings.video.frame_rate, loaded.video.frame_rate); assert_eq!(settings.daemon.auto_record, loaded.daemon.auto_record); } #[test] fn test_load_nonexistent_config() { let dir = tempdir().unwrap(); let config_path = dir.path().join("nonexistent.toml"); let persistence = ConfigPersistence::new(config_path); let settings = persistence.load().unwrap(); assert_eq!(settings.video.frame_rate, 60); } }