use std::path::Path; use anyhow::{Context as _, Result}; use jiff::Timestamp; use sha2::{Digest as _, Sha256}; use crate::config::{Config, DestinationConfig, RepoConfig}; use crate::git::MirrorRepo; use crate::s3::{Manifest, S3Destination}; /// Syncs a single repository to all of its configured destinations. pub async fn sync_repo(config: &Config, repo: &RepoConfig, cache_dir: &Path) -> Result<()> { println!("[{}] fetching from {}", repo.name, repo.source); let mirror_path = cache_dir.join("repos").join(format!("{}.git", repo.name)); let mirror = MirrorRepo::clone_or_update(&repo.source, &mirror_path) .with_context(|| format!("failed to mirror {}", repo.source))?; for destination_name in &repo.destinations { let destination = &config.destinations[destination_name]; match destination { DestinationConfig::Git { url } => { let url = url.replace("{name}", &repo.name); println!("[{}] pushing to {url}", repo.name); mirror.push_mirror(&url).with_context(|| { format!("failed to push to destination {destination_name:?}") })?; } DestinationConfig::S3 { bucket, endpoint, region, prefix, } => { backup_to_s3( repo, &mirror, destination_name, bucket, endpoint.as_deref(), region.as_deref(), prefix.as_deref(), ) .await .with_context(|| { format!("failed to back up to destination {destination_name:?}") })?; } } } Ok(()) } async fn backup_to_s3( repo: &RepoConfig, mirror: &MirrorRepo<'_>, destination_name: &str, bucket: &str, endpoint: Option<&str>, region: Option<&str>, prefix: Option<&str>, ) -> Result<()> { let refs_snapshot = mirror.refs_snapshot()?; if refs_snapshot.is_empty() { println!("[{}] repository is empty, skipping backup", repo.name); return Ok(()); } let refs_hash = hex(&Sha256::digest(refs_snapshot.as_bytes())); let destination = S3Destination::new(bucket, endpoint, region, prefix).await; if let Some(manifest) = destination.manifest(&repo.name).await? { if manifest.refs_hash == refs_hash { println!( "[{}] refs unchanged since {}, skipping backup to {destination_name:?}", repo.name, manifest.timestamp ); return Ok(()); } } let bundle_dir = std::env::temp_dir().join("kalandra"); std::fs::create_dir_all(&bundle_dir).context("failed to create temporary directory")?; let bundle_path = bundle_dir.join(format!("{}.bundle", repo.name)); println!("[{}] creating bundle", repo.name); mirror.create_bundle(&bundle_path)?; let timestamp = Timestamp::now(); let bundle_key = destination.bundle_key( &repo.name, ×tamp.strftime("%Y%m%dT%H%M%SZ").to_string(), &refs_hash, ); println!("[{}] uploading {bundle_key}", repo.name); let upload_result = destination.upload_bundle(&bundle_key, &bundle_path).await; std::fs::remove_file(&bundle_path).ok(); upload_result?; destination .put_manifest( &repo.name, &Manifest { refs_hash, bundle_key, timestamp: timestamp.to_string(), }, ) .await?; Ok(()) } fn hex(bytes: &[u8]) -> String { bytes.iter().map(|byte| format!("{byte:02x}")).collect() }