use std::collections::BTreeMap;
use std::time::Duration;
use crate::error::ShunError;
use crate::flow::{FlowEvent, FlowLog, FlowPhase};
pub fn agent() -> ureq::Agent {
ureq::AgentBuilder::new()
.timeout(Duration::from_secs(10))
.build()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWatch {
pub source: String,
pub files: BTreeMap<String, String>,
}
pub fn resolve(
sources: &[String],
files: &[String],
on_event: &mut dyn FnMut(FlowEvent),
) -> Option<ResolvedWatch> {
let files: Vec<String> = files
.iter()
.map(|file| file.trim().to_string())
.filter(|file| !file.is_empty())
.collect();
if files.is_empty() {
return None;
}
let agent = agent();
for source in sources {
let base = source.trim().trim_end_matches('/');
if base.is_empty() {
continue;
}
let probe_url = format!("{base}/{}", files[0]);
on_event(FlowEvent::Progress {
phase: FlowPhase::Download,
step: format!("Checking {}", host_of(base)),
percent: None,
});
match agent.get(&probe_url).call() {
Ok(response) if response.status() == 200 => {
on_event(FlowEvent::Progress {
phase: FlowPhase::Download,
step: format!("Using {base} …"),
percent: None,
});
return Some(ResolvedWatch {
source: base.to_string(),
files: files
.iter()
.map(|file| (file.clone(), format!("{base}/{file}")))
.collect(),
});
}
Ok(response) => skip(
on_event,
base,
&format!("probe answered {}", response.status()),
),
Err(ureq::Error::Status(status, _)) => {
skip(on_event, base, &format!("probe answered {status}"));
}
Err(err) => skip(on_event, base, &format!("probe failed: {err}")),
}
}
None
}
fn skip(on_event: &mut dyn FnMut(FlowEvent), base: &str, reason: &str) {
on_event(FlowEvent::Log {
record: FlowLog::Warning {
code: "update-source-skipped".to_string(),
detail: format!("{base}: {reason}"),
},
});
}
pub fn fetch_text(url: &str) -> Result<String, ShunError> {
let response = agent()
.get(url)
.call()
.map_err(|e| ShunError::Config(format!("fetch {url}: {e}")))?;
if response.status() != 200 {
return Err(ShunError::Config(format!(
"fetch {url}: unexpected status {}",
response.status()
)));
}
response
.into_string()
.map(|text| text.trim().to_string())
.map_err(|e| ShunError::Config(format!("fetch {url}: {e}")))
}
fn host_of(base: &str) -> &str {
let rest = base.split_once("://").map(|(_, rest)| rest).unwrap_or(base);
rest.split('/').next().unwrap_or(rest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_of_extracts_the_host_label() {
assert_eq!(
host_of("https://mirror.example.test/files"),
"mirror.example.test"
);
assert_eq!(host_of("http://127.0.0.1:8080"), "127.0.0.1:8080");
assert_eq!(host_of("127.0.0.1:8080"), "127.0.0.1:8080");
}
}