Something went wrong. Try again.
Mirror your Git repositories
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126use std::path::Path;
use anyhow::{Context as _, Result};use aws_config::Region;use aws_sdk_s3::primitives::ByteStream;use serde::{Deserialize, Serialize};
/// The manifest stored at `<prefix>/<name>/latest.json`, recording the most/// recent backup so that unchanged repositories can be skipped.#[derive(Debug, Serialize, Deserialize)]pub struct Manifest { /// A SHA-256 hash of the repository's refs at the time of the backup. pub refs_hash: String, /// The key of the most recent bundle. pub bundle_key: String, /// When the backup was taken, as an RFC 3339 timestamp. pub timestamp: String,}
pub struct S3Destination { client: aws_sdk_s3::Client, bucket: String, prefix: String,}
impl S3Destination { pub async fn new( bucket: &str, endpoint: Option<&str>, region: Option<&str>, prefix: Option<&str>, ) -> Self { let sdk_config = aws_config::defaults(aws_config::BehaviorVersion::latest()) .region(Region::new(region.unwrap_or("auto").to_string())) .load() .await; let mut builder = aws_sdk_s3::config::Builder::from(&sdk_config).force_path_style(true); if let Some(endpoint) = endpoint { builder = builder.endpoint_url(endpoint); }
Self { client: aws_sdk_s3::Client::from_conf(builder.build()), bucket: bucket.to_string(), prefix: prefix.unwrap_or("repos").trim_matches('/').to_string(), } }
fn manifest_key(&self, repo_name: &str) -> String { format!("{}/{}/latest.json", self.prefix, repo_name) }
pub fn bundle_key(&self, repo_name: &str, timestamp: &str, refs_hash: &str) -> String { format!( "{}/{}/{}-{}.bundle", self.prefix, repo_name, timestamp, &refs_hash[..8.min(refs_hash.len())] ) }
/// Fetches the manifest for the given repository, if one exists. pub async fn manifest(&self, repo_name: &str) -> Result<Option<Manifest>> { let response = self .client .get_object() .bucket(&self.bucket) .key(self.manifest_key(repo_name)) .send() .await; let output = match response { Ok(output) => output, Err(err) => { if err .as_service_error() .is_some_and(|err| err.is_no_such_key()) { return Ok(None); }
return Err(err).context("failed to fetch manifest"); } };
let bytes = output .body .collect() .await .context("failed to read manifest body")? .into_bytes(); let manifest = serde_json::from_slice(&bytes).context("failed to parse manifest")?; Ok(Some(manifest)) }
pub async fn upload_bundle(&self, key: &str, bundle_path: &Path) -> Result<()> { let body = ByteStream::from_path(bundle_path) .await .context("failed to open bundle for upload")?; self.client .put_object() .bucket(&self.bucket) .key(key) .content_type("application/octet-stream") .body(body) .send() .await .with_context(|| format!("failed to upload bundle to {key:?}"))?; Ok(()) }
pub async fn put_manifest(&self, repo_name: &str, manifest: &Manifest) -> Result<()> { let body = serde_json::to_vec_pretty(manifest).context("failed to serialize manifest")?; self.client .put_object() .bucket(&self.bucket) .key(self.manifest_key(repo_name)) .content_type("application/json") .body(ByteStream::from(body)) .send() .await .context("failed to upload manifest")?; Ok(()) }}