use super::lockfile::{CargoLock, CargoLockPackage};
use crate::core::lock::FileLock;
use crate::core::source_store::SourceStore;
use anyhow::{bail, Context, Result};
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Clone, Default)]
pub struct FetchOptions {
pub offline: bool,
pub locked: bool,
pub frozen: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FetchResult {
pub total_packages: usize,
pub already_cached: usize,
pub fetched: usize,
pub duration_ms: u64,
}
pub struct CrateFetcher {
store: SourceStore,
agent: ureq::Agent,
}
impl CrateFetcher {
pub fn new(store: SourceStore) -> Self {
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(30))
.build();
Self { store, agent }
}
pub fn store(&self) -> &SourceStore {
&self.store
}
pub fn fetch_package(
&self,
pkg: &CargoLockPackage,
opt: &FetchOptions,
) -> Result<Option<PathBuf>> {
if pkg.source.is_none() {
return Ok(None);
}
if self.store.has_source(&pkg.name, &pkg.version) {
return Ok(Some(self.store.package_dir(&pkg.name, &pkg.version)));
}
let lock_path = self.store.lock_path(&pkg.name, &pkg.version);
let _lock = FileLock::acquire_exclusive(&lock_path).with_context(|| {
format!(
"Failed to acquire download lock for {}-{}",
pkg.name, pkg.version
)
})?;
if self.store.has_source(&pkg.name, &pkg.version) {
return Ok(Some(self.store.package_dir(&pkg.name, &pkg.version)));
}
let expected_checksum = pkg.checksum.as_deref().unwrap_or("");
if let Some(path) = self.find_in_cargo_registry_cache(&pkg.name, &pkg.version) {
if let Ok(bytes) = fs::read(&path) {
if let Ok(dir) =
self.store
.unpack_archive(&pkg.name, &pkg.version, &bytes, expected_checksum)
{
return Ok(Some(dir));
}
}
}
if let Some(src_dir) = self.find_in_cargo_registry_src(&pkg.name, &pkg.version) {
let target_dir = self.store.package_dir(&pkg.name, &pkg.version);
if SourceStore::copy_dir_recursive(&src_dir, &target_dir).is_ok() {
return Ok(Some(target_dir));
}
}
if opt.offline {
bail!(
"Package {}-{} not found in local cache and offline mode was requested",
pkg.name,
pkg.version
);
}
let base_url = std::env::var("MITTS_CRATES_IO_URL")
.unwrap_or_else(|_| "https://static.crates.io/crates".to_string());
let url = format!(
"{}/{}/{}-{}.crate",
base_url.trim_end_matches('/'),
pkg.name,
pkg.name,
pkg.version
);
let mut body_bytes = Vec::new();
self.agent
.get(&url)
.call()
.with_context(|| format!("Failed to download crate from {}", url))?
.into_reader()
.read_to_end(&mut body_bytes)
.with_context(|| format!("Failed to read crate bytes from {}", url))?;
let dir =
self.store
.unpack_archive(&pkg.name, &pkg.version, &body_bytes, expected_checksum)?;
Ok(Some(dir))
}
pub fn fetch_all(&self, lock: &CargoLock, opt: &FetchOptions) -> Result<FetchResult> {
let start = Instant::now();
let mut total_packages = 0;
let mut already_cached = 0;
let mut to_fetch = Vec::new();
for pkg in &lock.packages {
if pkg.source.is_none() {
continue;
}
total_packages += 1;
if self.store.has_source(&pkg.name, &pkg.version) {
already_cached += 1;
} else {
to_fetch.push(pkg.clone());
}
}
let mut fetched = 0;
if !to_fetch.is_empty() {
let pb = std::sync::Arc::new(crate::cli::Ui::create_progress_bar(
to_fetch.len() as u64,
"📦 Fetching",
));
let num_threads = std::cmp::min(to_fetch.len(), 8);
let mut chunks = vec![Vec::new(); num_threads];
for (i, pkg) in to_fetch.into_iter().enumerate() {
chunks[i % num_threads].push(pkg);
}
std::thread::scope(|s| {
let mut handles = Vec::new();
for chunk in chunks {
let fetcher = self;
let opt_ref = opt;
let pb_ref = std::sync::Arc::clone(&pb);
handles.push(s.spawn(move || {
let mut count = 0;
for pkg in chunk {
pb_ref.set_message(format!("{} v{}", pkg.name, pkg.version));
if fetcher.fetch_package(&pkg, opt_ref).is_ok() {
count += 1;
}
pb_ref.inc(1);
}
count
}));
}
for h in handles {
fetched += h.join().unwrap_or(0);
}
});
pb.finish_and_clear();
}
let duration_ms = start.elapsed().as_millis() as u64;
Ok(FetchResult {
total_packages,
already_cached,
fetched,
duration_ms,
})
}
fn cargo_registry_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Ok(ch) = std::env::var("CARGO_HOME") {
dirs.push(PathBuf::from(ch).join("registry"));
}
if let Ok(home) = std::env::var("HOME") {
let p = PathBuf::from(home).join(".cargo").join("registry");
if !dirs.contains(&p) {
dirs.push(p);
}
}
dirs
}
fn find_in_cargo_registry_cache(&self, name: &str, version: &str) -> Option<PathBuf> {
let file_name = format!("{}-{}.crate", name, version);
for reg_dir in Self::cargo_registry_dirs() {
let cache_root = reg_dir.join("cache");
if let Ok(entries) = fs::read_dir(&cache_root) {
for entry in entries.flatten() {
let candidate = entry.path().join(&file_name);
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
fn find_in_cargo_registry_src(&self, name: &str, version: &str) -> Option<PathBuf> {
let dir_name = format!("{}-{}", name, version);
for reg_dir in Self::cargo_registry_dirs() {
let src_root = reg_dir.join("src");
if let Ok(entries) = fs::read_dir(&src_root) {
for entry in entries.flatten() {
let candidate = entry.path().join(&dir_name);
if candidate.is_dir() && candidate.join("Cargo.toml").is_file() {
return Some(candidate);
}
}
}
}
None
}
}