Something went wrong. Try again.
Mirror your Git repositories
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119use 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()}