| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | | use crate::target::Target; |
| 5 | | use anyhow::{bail, Context, Result}; |
| 6 | | use serde::de::DeserializeOwned; |
| 7 | | use serde::Serialize; |
| 8 | | use std::time::Duration; |
| 9 | |
|
| 10 | | pub struct Client { |
| 11 | | agent: ureq::Agent, |
| 12 | | base: String, |
| 13 | | token: Option<String>, |
| 14 | | } |
| 15 | |
|
| 16 | | impl Client { |
| 17 | 46× | pub fn new(t: &Target) -> Result<Client> { |
| 18 | 46× | let tls = oxy_core::protocol::tls_client_config(t.ca.as_deref()).map_err(|e| anyhow::anyhow!("{e}"))?; |
| 19 | 46× | let agent = ureq::AgentBuilder::new().tls_config(tls).timeout(Duration::from_secs(120)).build(); |
| 20 | 46× | Ok(Client { agent, base: format!("{}/api/v1", t.hub), token: t.token.clone() }) |
| 21 | 46× | } |
| 22 | |
|
| 23 | 47× | fn req(&self, method: &str, path: &str) -> ureq::Request { |
| 24 | 47× | let r = self.agent.request(method, &format!("{}{path}", self.base)).set("Accept", "application/json"); |
| 25 | 47× | match &self.token { |
| 26 | 45× | Some(t) => r.set("Authorization", &format!("Bearer {t}")), |
| 27 | 2× | None => r, |
| 28 | | } |
| 29 | 47× | } |
| 30 | |
|
| 31 | 47× | fn finish(res: Result<ureq::Response, ureq::Error>, what: &str) -> Result<String> { |
| 32 | 4× | match res { |
| 33 | 43× | Ok(r) => Ok(r.into_string()?), |
| 34 | 4× | Err(ureq::Error::Status(code, r)) => { |
| 35 | 4× | let body = r.into_string().unwrap_or_default(); |
| 36 | | |
| 37 | 4× | let detail = serde_json::from_str::<serde_json::Value>(&body) |
| 38 | 4× | .ok() |
| 39 | 4× | .and_then(|v| v["detail"].as_str().map(String::from)) |
| 40 | 4× | .unwrap_or(body); |
| 41 | 4× | let hint = match code { |
| 42 | 1× | 401 => " (sign in: `oh auth login`, or push with a token first)", |
| 43 | 0× | 403 => " (your token lacks the scope or the role for this)", |
| 44 | 3× | _ => "", |
| 45 | | }; |
| 46 | 4× | bail!("{what}: HTTP {code}: {}{hint}", detail.trim()) |
| 47 | | } |
| 48 | 0× | Err(e) => Err(e).context(what.to_string()), |
| 49 | | } |
| 50 | 47× | } |
| 51 | |
|
| 52 | | |
| 53 | 24× | pub fn get_text(&self, path: &str) -> Result<String> { |
| 54 | 24× | Self::finish(self.req("GET", path).call(), &format!("GET {path}")) |
| 55 | 24× | } |
| 56 | |
|
| 57 | 16× | pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> { |
| 58 | 16× | let text = self.get_text(path)?; |
| 59 | 15× | serde_json::from_str(&text).with_context(|| format!("GET {path}: unexpected answer")) |
| 60 | 16× | } |
| 61 | |
|
| 62 | 22× | pub fn send_text(&self, method: &str, path: &str, body: &impl Serialize) -> Result<String> { |
| 63 | 22× | let json = serde_json::to_string(body)?; |
| 64 | 22× | Self::finish(self.req(method, path).set("Content-Type", "application/json").send_string(&json), &format!("{method} {path}")) |
| 65 | 22× | } |
| 66 | |
|
| 67 | 17× | pub fn send<T: DeserializeOwned>(&self, method: &str, path: &str, body: &impl Serialize) -> Result<T> { |
| 68 | 17× | let text = self.send_text(method, path, body)?; |
| 69 | 14× | serde_json::from_str(&text).with_context(|| format!("{method} {path}: unexpected answer")) |
| 70 | 17× | } |
| 71 | |
|
| 72 | | |
| 73 | | |
| 74 | 0× | pub fn post_raw(&self, path: &str, body: &impl Serialize) -> Result<(u16, String)> { |
| 75 | 0× | let json = serde_json::to_string(body)?; |
| 76 | 0× | match self.req("POST", path).set("Content-Type", "application/json").send_string(&json) { |
| 77 | 0× | Ok(r) => Ok((r.status(), r.into_string()?)), |
| 78 | 0× | Err(ureq::Error::Status(code, r)) => Ok((code, r.into_string().unwrap_or_default())), |
| 79 | 0× | Err(e) => Err(e).context(format!("POST {path}")), |
| 80 | | } |
| 81 | 0× | } |
| 82 | |
|
| 83 | | |
| 84 | 0× | pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>> { |
| 85 | 0× | match self.req("GET", path).call() { |
| 86 | 0× | Ok(r) => { |
| 87 | 0× | let mut out = Vec::new(); |
| 88 | 0× | std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; |
| 89 | 0× | Ok(out) |
| 90 | | } |
| 91 | 0× | Err(e) => Self::finish(Err(e), &format!("GET {path}")).map(String::into_bytes), |
| 92 | | } |
| 93 | 0× | } |
| 94 | |
|
| 95 | | |
| 96 | 0× | pub fn get_root_bytes(&self, path: &str) -> Result<Vec<u8>> { |
| 97 | 0× | let hub = self.base.trim_end_matches("/api/v1"); |
| 98 | 0× | let r = self.agent.get(&format!("{hub}{path}")).call().with_context(|| format!("GET {path}"))?; |
| 99 | 0× | let mut out = Vec::new(); |
| 100 | 0× | std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; |
| 101 | 0× | Ok(out) |
| 102 | 0× | } |
| 103 | |
|
| 104 | | |
| 105 | 1× | pub fn upload(&self, path: &str, bytes: &[u8]) -> Result<String> { |
| 106 | 1× | Self::finish( |
| 107 | 1× | self.req("POST", path).set("Content-Type", "application/octet-stream").timeout(Duration::from_secs(1800)).send_bytes(bytes), |
| 108 | 1× | &format!("POST {path}"), |
| 109 | | ) |
| 110 | 1× | } |
| 111 | | } |