use crate::auth::SecureString;
use crate::lfs::pointer::LfsPointer;
use anyhow::{bail, Context, Result};
use reqwest::Client;
use sha2::{Digest, Sha256};
use std::path::Path;
pub struct LfsDownloader {
client: Client,
}
impl LfsDownloader {
pub fn new() -> Self {
Self {
client: Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()
.expect("failed to build HTTP client"),
}
}
pub async fn download_object(
&self,
repo_url: &str,
pointer: &LfsPointer,
dest: &Path,
token: Option<&SecureString>,
) -> Result<()> {
let lfs_url = format!(
"{}/info/lfs/objects/batch",
repo_url.trim_end_matches(".git")
);
let body = serde_json::json!({
"operation": "download",
"transfers": ["basic"],
"objects": [{
"oid": pointer.oid,
"size": pointer.size,
}]
});
let mut req = self
.client
.post(&lfs_url)
.header("Content-Type", "application/vnd.git-lfs+json")
.header("Accept", "application/vnd.git-lfs+json")
.json(&body);
if let Some(tok) = token {
req = req.header("Authorization", format!("token {}", tok.as_str()));
}
let response = req.send().await.context("LFS batch request failed")?;
if !response.status().is_success() {
bail!("LFS batch API returned HTTP {}", response.status());
}
let batch_resp: serde_json::Value = response.json().await?;
let download_url = batch_resp["objects"][0]["actions"]["download"]["href"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No download URL in LFS batch response"))?;
if !download_url.starts_with("https://") {
bail!("LFS download URL must be HTTPS, got: {}", download_url);
}
let obj_response = self
.client
.get(download_url)
.send()
.await
.context("LFS object download failed")?;
if !obj_response.status().is_success() {
bail!(
"LFS object download returned HTTP {}",
obj_response.status()
);
}
let bytes = obj_response.bytes().await?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = hex::encode(hasher.finalize());
if hash != pointer.oid {
bail!(
"LFS object hash mismatch: expected {}, got {}",
pointer.oid,
hash
);
}
tokio::fs::write(dest, &bytes).await?;
Ok(())
}
}
impl Default for LfsDownloader {
fn default() -> Self {
Self::new()
}
}