use std::path::Path;
use std::process::Command;
use std::time::Duration;
const MAX_BYTES: &str = "8388608";
const SAMPLE: Duration = Duration::from_millis(120);
pub(crate) fn bytes(url: &str, user_agent: &str) -> Result<Vec<u8>, String> {
if !url.starts_with("https://") {
return Err("refusing non-https url".to_string());
}
let out = Command::new("curl")
.args([
"-sL",
"--fail",
"--proto",
"=https",
"--proto-redir",
"=https",
"--max-filesize",
MAX_BYTES,
"-H",
"Accept: application/vnd.github+json",
"-H",
])
.arg(format!("User-Agent: {user_agent}"))
.args(["--", url])
.output()
.map_err(|e| format!("curl: {e}"))?;
if out.status.success() {
Ok(out.stdout)
} else {
Err(format!("fetch failed ({})", out.status))
}
}
pub(crate) fn file(
url: &str,
dest: &Path,
expected: u64,
user_agent: &str,
on_progress: &dyn Fn(u64),
) -> Result<(), String> {
if !url.starts_with("https://") {
return Err("refusing non-https download".to_string());
}
let _ = std::fs::remove_file(dest);
let mut child = Command::new("curl")
.args([
"-sL",
"--fail",
"--proto",
"=https",
"--proto-redir",
"=https",
"--connect-timeout",
"30",
"--speed-limit",
"1024",
"--speed-time",
"60",
"-o",
])
.arg(dest)
.arg("-H")
.arg(format!("User-Agent: {user_agent}"))
.args(["--", url])
.spawn()
.map_err(|e| format!("curl: {e}"))?;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) => {
on_progress(std::fs::metadata(dest).map(|m| m.len()).unwrap_or(0));
std::thread::sleep(SAMPLE);
}
Err(e) => {
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(dest);
return Err(format!("curl: {e}"));
}
}
};
if !status.success() {
let _ = std::fs::remove_file(dest);
return Err(format!("download failed ({status})"));
}
let got = std::fs::metadata(dest).map(|m| m.len()).unwrap_or(0);
if expected > 0 && got != expected {
let _ = std::fs::remove_file(dest);
return Err(format!(
"download is incomplete ({got} of {expected} bytes)"
));
}
on_progress(got);
Ok(())
}