qsm_core/models/
download.rs1use std::fs;
9use std::io::{Read, Write};
10use std::path::PathBuf;
11
12use sha2::{Digest, Sha256};
13
14use super::{cache_dir, cache_path, resolve_local, ModelSpec, WeightFile};
15
16#[derive(Debug)]
18pub enum DownloadError {
19 NotHosted { model: String, file: String },
21 Http(String),
23 Io(std::io::Error),
25 ChecksumMismatch { file: String, expected: String, got: String },
27}
28
29impl std::fmt::Display for DownloadError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::NotHosted { model, file } => write!(
33 f,
34 "weights for '{model}' are not hosted yet (file '{file}'). \
35 Supply it locally via $QSM_MODEL_DIR."
36 ),
37 Self::Http(msg) => write!(f, "download failed: {msg}"),
38 Self::Io(e) => write!(f, "io error: {e}"),
39 Self::ChecksumMismatch { file, expected, got } => write!(
40 f,
41 "checksum mismatch for '{file}': expected {expected}, got {got}"
42 ),
43 }
44 }
45}
46
47impl std::error::Error for DownloadError {}
48
49impl From<std::io::Error> for DownloadError {
50 fn from(e: std::io::Error) -> Self {
51 Self::Io(e)
52 }
53}
54
55pub fn sha256_hex(bytes: &[u8]) -> String {
57 let mut hasher = Sha256::new();
58 hasher.update(bytes);
59 let digest = hasher.finalize();
60 let mut out = String::with_capacity(64);
61 for b in digest {
62 use std::fmt::Write as _;
63 let _ = write!(out, "{b:02x}");
64 }
65 out
66}
67
68pub fn ensure_file(model_id: &str, file: &WeightFile) -> Result<PathBuf, DownloadError> {
71 ensure_file_with_progress(model_id, file, &mut |_, _| {})
72}
73
74pub fn ensure_file_with_progress(
81 model_id: &str,
82 file: &WeightFile,
83 on_progress: &mut dyn FnMut(u64, u64),
84) -> Result<PathBuf, DownloadError> {
85 if let Some(path) = resolve_local(file) {
87 if file.sha256.is_empty() || checksum_ok(&path, file.sha256)? {
88 return Ok(path);
89 }
90 }
92
93 if file.url.is_empty() {
95 return Err(DownloadError::NotHosted {
96 model: model_id.to_string(),
97 file: file.name.to_string(),
98 });
99 }
100
101 let bytes = http_get(file.url, file.bytes, on_progress)?;
102
103 if !file.sha256.is_empty() {
105 let got = sha256_hex(&bytes);
106 if got != file.sha256 {
107 return Err(DownloadError::ChecksumMismatch {
108 file: file.name.to_string(),
109 expected: file.sha256.to_string(),
110 got,
111 });
112 }
113 }
114
115 let dir = cache_dir();
117 fs::create_dir_all(&dir)?;
118 let final_path = cache_path(file);
119 let tmp = dir.join(format!("{}.part", file.name));
120 {
121 let mut f = fs::File::create(&tmp)?;
122 f.write_all(&bytes)?;
123 f.sync_all()?;
124 }
125 fs::rename(&tmp, &final_path)?;
126 Ok(final_path)
127}
128
129pub fn ensure_model(spec: &ModelSpec) -> Result<Vec<PathBuf>, DownloadError> {
132 spec.files.iter().map(|f| ensure_file(spec.id, f)).collect()
133}
134
135pub fn primary_bytes(spec: &ModelSpec) -> Result<Vec<u8>, DownloadError> {
138 let file = spec.files.first().ok_or_else(|| DownloadError::NotHosted {
139 model: spec.id.to_string(),
140 file: "<none>".to_string(),
141 })?;
142 let path = ensure_file(spec.id, file)?;
143 Ok(fs::read(path)?)
144}
145
146fn checksum_ok(path: &PathBuf, expected: &str) -> Result<bool, DownloadError> {
147 let bytes = fs::read(path)?;
148 Ok(sha256_hex(&bytes) == expected)
149}
150
151const MAX_ATTEMPTS: u32 = 5;
153
154fn http_get(
165 url: &str,
166 expected_total: u64,
167 on_progress: &mut dyn FnMut(u64, u64),
168) -> Result<Vec<u8>, DownloadError> {
169 let mut attempt = 0;
170 loop {
171 attempt += 1;
172 match http_get_once(url, expected_total, on_progress) {
173 Ok(bytes) => return Ok(bytes),
174 Err((msg, retryable)) => {
175 if !retryable || attempt >= MAX_ATTEMPTS {
176 return Err(DownloadError::Http(format!("{msg} (after {attempt} attempt(s))")));
177 }
178 let base = 500u64 << (attempt - 1);
180 let jitter = (url.bytes().map(u64::from).sum::<u64>() % 500) + u64::from(attempt) * 37;
181 std::thread::sleep(std::time::Duration::from_millis(base + jitter));
182 }
183 }
184 }
185}
186
187fn http_get_once(
189 url: &str,
190 expected_total: u64,
191 on_progress: &mut dyn FnMut(u64, u64),
192) -> Result<Vec<u8>, (String, bool)> {
193 match ureq::get(url).call() {
194 Ok(resp) => {
195 let total = if expected_total > 0 {
196 expected_total
197 } else {
198 resp.header("Content-Length").and_then(|s| s.parse().ok()).unwrap_or(0)
199 };
200 let mut reader = resp.into_reader();
201 let mut bytes: Vec<u8> = Vec::with_capacity(total as usize);
202 let mut buf = [0u8; 64 * 1024];
203 on_progress(0, total);
204 loop {
205 match reader.read(&mut buf) {
206 Ok(0) => break,
207 Ok(n) => {
208 bytes.extend_from_slice(&buf[..n]);
209 let done = bytes.len() as u64;
210 on_progress(done, total.max(done));
211 }
212 Err(e) => return Err((e.to_string(), true)),
214 }
215 }
216 Ok(bytes)
217 }
218 Err(ureq::Error::Status(code, _)) => {
220 let retryable = matches!(code, 403 | 408 | 425 | 429 | 500 | 502 | 503 | 504);
221 Err((format!("status code {code}"), retryable))
222 }
223 Err(e @ ureq::Error::Transport(_)) => Err((e.to_string(), true)),
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn sha256_matches_known_vector() {
234 assert_eq!(
236 sha256_hex(b""),
237 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
238 );
239 assert_eq!(
241 sha256_hex(b"abc"),
242 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
243 );
244 }
245}