use std::process::Command;
const RETRY_ATTEMPTS: &str = "3";
const RETRY_DELAY_SECS: &str = "2";
const CONNECT_TIMEOUT_SECS: &str = "30";
const fn retry_args() -> [&'static str; 8] {
[
"--retry",
RETRY_ATTEMPTS,
"--retry-delay",
RETRY_DELAY_SECS,
"--retry-connrefused",
"--retry-all-errors",
"--connect-timeout",
CONNECT_TIMEOUT_SECS,
]
}
pub fn curl_argv(url: &str, dest: &str) -> Vec<String> {
curl_argv_with_token_var(url, dest, github_token_var())
}
fn curl_argv_with_token_var(url: &str, dest: &str, token_var: Option<&str>) -> Vec<String> {
let mut argv = vec!["curl".to_string(), "-fsSL".to_string()];
argv.extend(retry_args().iter().map(|s| (*s).to_string()));
if let Some(var) = token_var
&& url.starts_with("https://raw.githubusercontent.com/")
{
argv.extend([
"--variable".to_string(),
format!("%{var}"),
"--expand-header".to_string(),
format!("Authorization: Bearer {{{{{var}}}}}"),
]);
}
argv.extend(["-o".to_string(), dest.to_string(), url.to_string()]);
argv
}
pub fn github_token_var() -> Option<&'static str> {
github_token_var_from(|name| std::env::var(name).ok())
}
fn github_token_var_from(lookup: impl Fn(&str) -> Option<String>) -> Option<&'static str> {
["GITHUB_TOKEN", "GH_TOKEN"]
.into_iter()
.find(|name| lookup(name).is_some_and(|token| !token.trim().is_empty()))
}
pub fn apply_retry_args(cmd: &mut Command) {
cmd.args(retry_args());
}
pub fn with_retry<T, E, F>(mut attempt: F) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let attempts: u32 = RETRY_ATTEMPTS.parse().unwrap_or(3);
let delay = std::time::Duration::from_secs(RETRY_DELAY_SECS.parse().unwrap_or(2));
let mut last = None;
for i in 0..attempts.max(1) {
if i > 0 {
std::thread::sleep(delay);
}
match attempt() {
Ok(value) => return Ok(value),
Err(err) => last = Some(err),
}
}
Err(last.expect("at least one attempt ran"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn curl_argv_includes_retry_flags() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert!(argv.contains(&"--retry".to_string()));
assert!(argv.contains(&"--retry-connrefused".to_string()));
assert!(argv.contains(&"--retry-all-errors".to_string()));
assert!(argv.contains(&"--connect-timeout".to_string()));
}
#[test]
fn curl_argv_puts_url_last_and_dest_after_o() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert_eq!(argv[0], "curl");
assert_eq!(argv.last().unwrap(), "https://example.com/x.gz");
let o = argv.iter().position(|a| a == "-o").expect("-o present");
assert_eq!(argv[o + 1], "/tmp/x.dl");
}
#[test]
fn raw_github_downloads_are_authenticated_by_variable_name() {
let url = "https://raw.githubusercontent.com/torvalds/linux/master/scripts/checkpatch.pl";
let argv = curl_argv_with_token_var(url, "/tmp/c.dl", Some("GITHUB_TOKEN"));
let at = argv
.iter()
.position(|a| a == "--variable")
.expect("--variable present");
assert_eq!(argv[at + 1], "%GITHUB_TOKEN");
let at = argv
.iter()
.position(|a| a == "--expand-header")
.expect("--expand-header present");
assert_eq!(argv[at + 1], "Authorization: Bearer {{GITHUB_TOKEN}}");
assert_eq!(argv.last().unwrap(), url, "the URL stays last");
let anonymous = curl_argv_with_token_var(url, "/tmp/c.dl", None);
assert!(!anonymous.contains(&"--variable".to_string()));
}
#[test]
fn other_hosts_get_no_token() {
for url in [
"https://example.com/x.gz",
"https://github.com/biomejs/biome/releases/latest/download/biome-linux-x64",
] {
let argv = curl_argv_with_token_var(url, "/tmp/x.dl", Some("GITHUB_TOKEN"));
assert!(!argv.contains(&"--expand-header".to_string()), "{url}");
}
}
#[test]
fn github_token_var_prefers_github_token_and_skips_empty() {
let lookup = |vars: &[(&str, &str)], name: &str| {
vars.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| (*v).to_string())
};
assert_eq!(
github_token_var_from(|n| lookup(&[("GITHUB_TOKEN", "a"), ("GH_TOKEN", "b")], n)),
Some("GITHUB_TOKEN")
);
assert_eq!(
github_token_var_from(|n| lookup(&[("GH_TOKEN", "b")], n)),
Some("GH_TOKEN")
);
assert_eq!(
github_token_var_from(|n| lookup(&[("GITHUB_TOKEN", ""), ("GH_TOKEN", "b")], n)),
Some("GH_TOKEN")
);
assert_eq!(
github_token_var_from(|n| lookup(&[("GITHUB_TOKEN", " ")], n)),
None
);
assert_eq!(github_token_var_from(|n| lookup(&[], n)), None);
}
#[test]
fn no_total_transfer_timeout() {
let argv = curl_argv("https://example.com/x.gz", "/tmp/x.dl");
assert!(!argv.contains(&"--max-time".to_string()));
}
#[test]
fn with_retry_returns_first_success_without_retrying() {
let mut calls = 0;
let out: Result<u8, ()> = with_retry(|| {
calls += 1;
Ok(7)
});
assert_eq!(out, Ok(7));
assert_eq!(calls, 1);
}
#[test]
fn with_retry_succeeds_after_a_transient_failure() {
let mut calls = 0;
let out: Result<u8, &str> = with_retry(|| {
calls += 1;
if calls < 2 { Err("reset") } else { Ok(9) }
});
assert_eq!(out, Ok(9));
assert_eq!(calls, 2);
}
#[test]
fn with_retry_gives_up_and_returns_last_error() {
let mut calls = 0;
let out: Result<u8, &str> = with_retry(|| {
calls += 1;
Err("down")
});
assert_eq!(out, Err("down"));
assert_eq!(calls, 3);
}
}