| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
use crate::home::Home; |
| 9 |
use anyhow::{bail, Context, Result}; |
| 10 |
use jet::images::{self, Chunk, Manifest}; |
| 11 |
use std::collections::HashSet; |
| 12 |
use std::path::{Path, PathBuf}; |
| 13 |
use std::sync::Mutex; |
| 14 |
use std::time::{Duration, Instant}; |
| 15 |
|
| 16 |
|
| 17 |
const PARALLEL: usize = 8; |
| 18 |
|
| 19 |
|
| 20 |
pub struct Client { |
| 21 |
base: String, |
| 22 |
token: String, |
| 23 |
agent: ureq::Agent, |
| 24 |
} |
| 25 |
|
| 26 |
|
| 27 |
pub struct Moved { |
| 28 |
pub chunks: usize, |
| 29 |
pub bytes: u64, |
| 30 |
pub elapsed: Duration, |
| 31 |
} |
| 32 |
|
| 33 |
impl Client { |
| 34 |
pub fn new(hub: &str, token: &str, ca: Option<&Path>) -> Result<Client> { |
| 35 |
let tls = oxy_core::protocol::tls_client_config(ca).map_err(|e| anyhow::anyhow!("{e}"))?; |
| 36 |
let agent = ureq::AgentBuilder::new().tls_config(tls).timeout(Duration::from_secs(120)).build(); |
| 37 |
Ok(Client { base: format!("{}/api/v1", hub.trim_end_matches('/')), token: token.into(), agent }) |
| 38 |
} |
| 39 |
|
| 40 |
fn auth(&self) -> String { |
| 41 |
format!("Bearer {}", self.token) |
| 42 |
} |
| 43 |
|
| 44 |
fn fail(url: &str, e: ureq::Error) -> anyhow::Error { |
| 45 |
match e { |
| 46 |
ureq::Error::Status(code, r) => { |
| 47 |
let text = r.into_string().unwrap_or_default(); |
| 48 |
let detail = serde_json::from_str::<serde_json::Value>(&text) |
| 49 |
.ok() |
| 50 |
.and_then(|v| v["detail"].as_str().map(String::from)) |
| 51 |
.unwrap_or(text); |
| 52 |
anyhow::anyhow!("{url}: HTTP {code}: {detail}") |
| 53 |
} |
| 54 |
e => anyhow::anyhow!("{url}: {e}"), |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
|
| 59 |
pub fn manifest(&self, hash: &str) -> Result<Option<Manifest>> { |
| 60 |
let url = format!("{}/flow/images/{hash}", self.base); |
| 61 |
match self.agent.get(&url).set("Authorization", &self.auth()).call() { |
| 62 |
Ok(r) => Ok(Some(r.into_json()?)), |
| 63 |
Err(ureq::Error::Status(404, _)) => Ok(None), |
| 64 |
Err(e) => Err(Self::fail(&url, e)), |
| 65 |
} |
| 66 |
} |
| 67 |
|
| 68 |
fn chunk(&self, hash: &str) -> Result<Vec<u8>> { |
| 69 |
let url = format!("{}/flow/images/chunks/{hash}", self.base); |
| 70 |
let r = self.agent.get(&url).set("Authorization", &self.auth()).call().map_err(|e| Self::fail(&url, e))?; |
| 71 |
let mut out = Vec::new(); |
| 72 |
std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; |
| 73 |
Ok(out) |
| 74 |
} |
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
pub fn push(&self, home: &Home, hash: &str) -> Result<Moved> { |
| 79 |
let start = Instant::now(); |
| 80 |
let path = home.rootfs(hash); |
| 81 |
if !path.is_file() { |
| 82 |
bail!("{} is not in this Flow home ({}); import it first", &hash[..12.min(hash.len())], path.display()); |
| 83 |
} |
| 84 |
let m = images::scan_file(&path, home.image_arch(hash), |_, _| Ok(()))?; |
| 85 |
if m.hash != hash { |
| 86 |
bail!("{} holds an image named {}, not {}: the file was changed", path.display(), &m.hash[..12], &hash[..12]); |
| 87 |
} |
| 88 |
let mut missing: HashSet<String> = HashSet::new(); |
| 89 |
for ask in m.needed().chunks(50_000) { |
| 90 |
let url = format!("{}/flow/images/missing", self.base); |
| 91 |
let r: serde_json::Value = self |
| 92 |
.agent |
| 93 |
.post(&url) |
| 94 |
.set("Authorization", &self.auth()) |
| 95 |
.send_json(serde_json::json!({ "chunks": ask })) |
| 96 |
.map_err(|e| Self::fail(&url, e))? |
| 97 |
.into_json()?; |
| 98 |
missing.extend(r["missing"].as_array().into_iter().flatten().filter_map(|h| h.as_str().map(String::from))); |
| 99 |
} |
| 100 |
|
| 101 |
let (count, bytes) = (missing.len(), Mutex::new(0u64)); |
| 102 |
let queue: Mutex<Vec<(String, Vec<u8>)>> = Mutex::new(Vec::new()); |
| 103 |
let failed: Mutex<Option<anyhow::Error>> = Mutex::new(None); |
| 104 |
let sent = Mutex::new(HashSet::new()); |
| 105 |
let send = |c: &str, b: &[u8]| -> Result<()> { |
| 106 |
let url = format!("{}/flow/images/chunks/{c}", self.base); |
| 107 |
self.agent.put(&url).set("Authorization", &self.auth()).send_bytes(b).map_err(|e| Self::fail(&url, e))?; |
| 108 |
*bytes.lock().unwrap() += b.len() as u64; |
| 109 |
Ok(()) |
| 110 |
}; |
| 111 |
images::scan_file(&path, None, |c: &Chunk, b: &[u8]| { |
| 112 |
if missing.contains(c.hash()) && sent.lock().unwrap().insert(c.hash().to_string()) { |
| 113 |
queue.lock().unwrap().push((c.hash().to_string(), b.to_vec())); |
| 114 |
} |
| 115 |
if queue.lock().unwrap().len() >= PARALLEL * 4 { |
| 116 |
let batch: Vec<_> = std::mem::take(&mut *queue.lock().unwrap()); |
| 117 |
send_batch(&batch, &send, &failed); |
| 118 |
} |
| 119 |
match failed.lock().unwrap().take() { |
| 120 |
Some(e) => Err(images::Error::Bad(format!("{e:#}"))), |
| 121 |
None => Ok(()), |
| 122 |
} |
| 123 |
})?; |
| 124 |
let rest: Vec<_> = std::mem::take(&mut *queue.lock().unwrap()); |
| 125 |
send_batch(&rest, &send, &failed); |
| 126 |
if let Some(e) = failed.lock().unwrap().take() { |
| 127 |
return Err(e); |
| 128 |
} |
| 129 |
let url = format!("{}/flow/images/{hash}", self.base); |
| 130 |
self.agent.put(&url).set("Authorization", &self.auth()).send_json(&m).map_err(|e| Self::fail(&url, e))?; |
| 131 |
let bytes = *bytes.lock().unwrap(); |
| 132 |
Ok(Moved { chunks: count, bytes, elapsed: start.elapsed() }) |
| 133 |
} |
| 134 |
|
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
pub fn fetch(&self, home: &Home, hash: &str) -> Result<Option<Moved>> { |
| 139 |
let start = Instant::now(); |
| 140 |
let Some(m) = self.manifest(hash)? else { return Ok(None) }; |
| 141 |
if m.hash != hash { |
| 142 |
bail!("the hub's manifest for {} names {}", &hash[..12], &m.hash[..12]); |
| 143 |
} |
| 144 |
let cache = chunk_cache(home); |
| 145 |
let wanted: Vec<String> = m.needed().into_iter().filter(|h| !cache_path(&cache, h).is_file()).map(String::from).collect(); |
| 146 |
let (bytes, failed) = (Mutex::new(0u64), Mutex::new(None::<anyhow::Error>)); |
| 147 |
let next = Mutex::new(wanted.iter()); |
| 148 |
std::thread::scope(|s| { |
| 149 |
for _ in 0..PARALLEL { |
| 150 |
s.spawn(|| loop { |
| 151 |
let Some(h) = next.lock().unwrap().next() else { break }; |
| 152 |
if failed.lock().unwrap().is_some() { |
| 153 |
break; |
| 154 |
} |
| 155 |
let got = self.chunk(h).and_then(|b| { |
| 156 |
if blake3::hash(&b).to_hex().as_str() != h { |
| 157 |
bail!("chunk {} came back with other bytes", &h[..12]); |
| 158 |
} |
| 159 |
let p = cache_path(&cache, h); |
| 160 |
std::fs::create_dir_all(p.parent().expect("a chunk directory"))?; |
| 161 |
let tmp = p.with_extension("part"); |
| 162 |
std::fs::write(&tmp, &b)?; |
| 163 |
std::fs::rename(&tmp, &p)?; |
| 164 |
*bytes.lock().unwrap() += b.len() as u64; |
| 165 |
Ok(()) |
| 166 |
}); |
| 167 |
if let Err(e) = got { |
| 168 |
*failed.lock().unwrap() = Some(e); |
| 169 |
break; |
| 170 |
} |
| 171 |
}); |
| 172 |
} |
| 173 |
}); |
| 174 |
if let Some(e) = failed.into_inner().unwrap() { |
| 175 |
return Err(e); |
| 176 |
} |
| 177 |
let out = home.rootfs(hash); |
| 178 |
std::fs::create_dir_all(out.parent().expect("the cas directory"))?; |
| 179 |
images::assemble(&m, &out, |c| Ok(std::fs::read(cache_path(&cache, c.hash()))?)) |
| 180 |
.with_context(|| format!("write image {}", &hash[..12]))?; |
| 181 |
if let Some(arch) = &m.arch { |
| 182 |
let manifest = serde_json::json!({ "arch": arch, "imported_ms": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0), "from": "hub" }); |
| 183 |
std::fs::write(home.image_manifest(hash), serde_json::to_vec(&manifest)?)?; |
| 184 |
} |
| 185 |
Ok(Some(Moved { chunks: wanted.len(), bytes: bytes.into_inner().unwrap(), elapsed: start.elapsed() })) |
| 186 |
} |
| 187 |
} |
| 188 |
|
| 189 |
fn send_batch(batch: &[(String, Vec<u8>)], send: &(dyn Fn(&str, &[u8]) -> Result<()> + Sync), failed: &Mutex<Option<anyhow::Error>>) { |
| 190 |
std::thread::scope(|s| { |
| 191 |
for part in batch.chunks(batch.len().div_ceil(PARALLEL).max(1)) { |
| 192 |
s.spawn(move || { |
| 193 |
for (h, b) in part { |
| 194 |
if let Err(e) = send(h, b) { |
| 195 |
*failed.lock().unwrap() = Some(e); |
| 196 |
return; |
| 197 |
} |
| 198 |
} |
| 199 |
}); |
| 200 |
} |
| 201 |
}); |
| 202 |
} |
| 203 |
|
| 204 |
|
| 205 |
pub fn chunk_cache(home: &Home) -> PathBuf { |
| 206 |
home.root.join("chunks") |
| 207 |
} |
| 208 |
|
| 209 |
fn cache_path(cache: &Path, hash: &str) -> PathBuf { |
| 210 |
cache.join(&hash[..2]).join(hash) |
| 211 |
} |
| 212 |
|
| 213 |
|
| 214 |
pub fn human(bytes: u64) -> String { |
| 215 |
if bytes >= 1 << 30 { |
| 216 |
format!("{:.1} GiB", bytes as f64 / (1u64 << 30) as f64) |
| 217 |
} else { |
| 218 |
format!("{} MiB", bytes >> 20) |
| 219 |
} |
| 220 |
} |