use serde::Deserialize; #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct ImportConfig { pub enabled: bool, pub token_secret: String, pub allowed_hosts: Vec, pub allow_insecure_tls: bool, pub max_concurrent_jobs: usize, pub clone_timeout_secs: u64, pub api_timeout_secs: u64, pub poll_interval_secs: u64, pub max_attempts: i32, pub max_repo_bytes: u64, pub stale_lock_secs: u64, } impl Default for ImportConfig { fn default() -> Self { Self { enabled: false, token_secret: String::new(), allowed_hosts: Vec::new(), allow_insecure_tls: false, max_concurrent_jobs: 2, clone_timeout_secs: 3600, api_timeout_secs: 30, poll_interval_secs: 3, max_attempts: 5, max_repo_bytes: 0, stale_lock_secs: 1800, } } } impl ImportConfig { pub fn validate(&self) -> Result<(), String> { if !self.enabled { return Ok(()); } if self.token_secret.trim().is_empty() { return Err("import.token_secret must not be empty when import is enabled".to_string()); } if self.max_concurrent_jobs == 0 || self.clone_timeout_secs == 0 || self.api_timeout_secs == 0 || self.poll_interval_secs == 0 || self.stale_lock_secs == 0 { return Err("import limits and intervals must be greater than zero".to_string()); } if self.max_attempts <= 0 { return Err("import.max_attempts must be greater than zero".to_string()); } for host in &self.allowed_hosts { let normalized = host.trim().to_ascii_lowercase(); if normalized.is_empty() || normalized.contains("://") || normalized.contains('/') { return Err(format!( "import.allowed_hosts entry `{host}` must be a bare host name" )); } } Ok(()) } pub fn host_allowed(&self, host: &str) -> bool { let host = host.trim().trim_end_matches('.').to_ascii_lowercase(); if host.is_empty() { return false; } self.allowed_hosts.iter().any(|entry| { let entry = entry.trim().trim_end_matches('.').to_ascii_lowercase(); !entry.is_empty() && (host == entry || host.ends_with(&format!(".{entry}"))) }) } }