use std::path::Path;
use std::process::Stdio;
use indicatif::{ProgressBar, ProgressStyle};
use crate::config::{Result, TestbedError};
pub fn download(url: &str, dest: &Path) -> Result<()> {
let partial = dest.with_extension(dest.extension().map_or_else(
|| "partial".to_string(),
|ext| format!("{}.partial", ext.to_string_lossy()),
));
if is_cached(dest) {
return Ok(());
}
let start_byte = if partial.exists() {
let metadata = std::fs::metadata(&partial).map_err(|e| TestbedError::DownloadFailed {
status: 0,
url: format!("stat {partial:?}: {e}"),
})?;
Some(metadata.len())
} else {
None
};
let mut cmd = std::process::Command::new("curl");
cmd.args([
"-L", "--fail", "--create-dirs",
"-o",
partial.to_str().unwrap(),
]);
if let Some(offset) = start_byte
&& offset > 0 {
cmd.args(["-C", "-"]);
}
cmd.args(["-s", "--show-error", url]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| TestbedError::DownloadFailed {
status: 0,
url: format!("curl not found: {e}"),
})?;
drop(child.stdout.take());
drop(child.stderr.take());
let status = child.wait().map_err(|e| TestbedError::DownloadFailed {
status: 0,
url: format!("curl wait error: {e}"),
})?;
if !status.success() {
let code = status.code().unwrap_or(0);
if start_byte.is_some() && start_byte.unwrap() > 0 {
let _ = std::fs::remove_file(&partial);
return download(url, dest); }
return Err(TestbedError::DownloadFailed {
status: code as u16,
url: url.to_string(),
});
}
let meta = std::fs::metadata(&partial).map_err(|e| TestbedError::DownloadFailed {
status: 0,
url: format!("stat {partial:?}: {e}"),
})?;
if meta.len() < 100 * 1024 * 1024 {
let _ = std::fs::remove_file(&partial);
return Err(TestbedError::DownloadFailed {
status: 0,
url: format!(
"downloaded file is {} bytes (< 100 MB threshold), likely corrupted or truncated",
meta.len()
),
});
}
std::fs::rename(&partial, dest).map_err(|e| TestbedError::DownloadFailed {
status: 0,
url: format!("rename {partial:?} → {dest:?}: {e}"),
})?;
let pb = ProgressBar::new(meta.len());
pb.set_position(meta.len());
pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})",
)
.unwrap()
.progress_chars("#>-"),
);
pb.finish_with_message("Download complete");
Ok(())
}
pub fn is_cached(dest: &Path) -> bool {
dest.exists()
&& std::fs::metadata(dest)
.map(|m| m.len() > 0)
.unwrap_or(false)
}