use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result, bail}; use serde::Deserialize; /// The top-level Kalandra configuration. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { /// The directory in which mirrored repositories are cached. /// /// Defaults to `~/.cache/kalandra`. pub cache_dir: Option, #[serde(default)] pub repos: Vec, #[serde(default)] pub destinations: BTreeMap, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct RepoConfig { /// The name of the repository (e.g., `kalandra`). pub name: String, /// The source of the repository: a Git URL or a local path. pub source: String, /// The names of the destinations to mirror this repository to. #[serde(default)] pub destinations: Vec, } #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] pub enum DestinationConfig { /// A generic Git remote (another forge, a bare server, etc.). Git { /// The push URL. `{name}` is replaced with the repository name. url: String, }, /// An S3-compatible object store (AWS S3, Cloudflare R2, etc.). S3 { bucket: String, /// A custom endpoint, e.g. `https://.r2.cloudflarestorage.com`. endpoint: Option, /// The region. Use `auto` for R2. Defaults to `auto`. region: Option, /// A key prefix under which bundles are stored. Defaults to `repos`. prefix: Option, }, } impl Config { pub fn load(path: &Path) -> Result { let contents = std::fs::read_to_string(path) .with_context(|| format!("failed to read config file: {}", path.display()))?; let config: Self = toml::from_str(&contents) .with_context(|| format!("failed to parse config file: {}", path.display()))?; config.validate()?; Ok(config) } fn validate(&self) -> Result<()> { for repo in &self.repos { for destination in &repo.destinations { if !self.destinations.contains_key(destination) { bail!( "repo {:?} references unknown destination {:?}", repo.name, destination ); } } } Ok(()) } pub fn cache_dir(&self) -> Result { if let Some(cache_dir) = &self.cache_dir { return Ok(cache_dir.clone()); } let cache_dir = dirs::cache_dir().context("failed to determine cache directory")?; Ok(cache_dir.join("kalandra")) } }