tiedcrossing_client/
entity.rs

1// SPDX-FileCopyrightText: 2022 Profian Inc. <opensource@profian.com>
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use super::{Client, Result};
5
6use std::io::{copy, Read, Write};
7use std::str::FromStr;
8
9use drawbridge_type::digest::{Algorithms, ContentDigest};
10use drawbridge_type::Meta;
11
12use anyhow::{anyhow, bail, Context};
13use http::header::{CONTENT_LENGTH, CONTENT_TYPE};
14use http::StatusCode;
15use mime::Mime;
16use ureq::serde::{Deserialize, Serialize};
17use ureq::{Request, Response};
18
19fn parse_header<T>(req: &Response, name: &str) -> Result<T>
20where
21    T: FromStr,
22    T::Err: 'static + Sync + Send + std::error::Error,
23{
24    req.header(name)
25        .ok_or_else(|| anyhow!("missing `{name}` header"))?
26        .parse()
27        .context(format!("failed to parse `{name}` header"))
28}
29
30#[derive(Clone, Debug)]
31pub struct Entity<'a> {
32    client: &'a Client,
33    path: String,
34}
35
36fn parse_ureq_error(e: ureq::Error) -> anyhow::Error {
37    match e {
38        ureq::Error::Status(code, msg) => match msg.into_string() {
39            Ok(msg) if !msg.is_empty() => {
40                anyhow!(msg).context(format!("request failed with status code `{code}`"))
41            }
42            _ => anyhow!("request failed with status code `{code}`"),
43        },
44
45        ureq::Error::Transport(e) => anyhow::Error::new(e).context("transport layer failure"),
46    }
47}
48
49impl<'a> Entity<'a> {
50    pub fn new(client: &'a Client) -> Self {
51        Self {
52            client,
53            path: Default::default(),
54        }
55    }
56
57    /// Returns a child [Entity] rooted at `path`.
58    pub fn child(&self, path: &str) -> Self {
59        Self {
60            client: self.client,
61            path: format!("{}/{}", self.path, path),
62        }
63    }
64
65    pub(super) fn create_request(&self, hash: &ContentDigest, mime: &Mime) -> Result<Request> {
66        let token = self.client.token.as_ref().ok_or_else(|| {
67            anyhow!("endpoint requires authorization, but no token was configured")
68        })?;
69        let url = self.client.url(&self.path)?;
70        Ok(self
71            .client
72            .inner
73            .put(url.as_str())
74            .set("Authorization", &format!("Bearer {token}"))
75            .set("Content-Digest", &hash.to_string())
76            .set(CONTENT_TYPE.as_str(), mime.as_ref()))
77    }
78
79    pub(super) fn create_bytes(&self, mime: &Mime, data: impl AsRef<[u8]>) -> Result<bool> {
80        let data = data.as_ref();
81        let (n, hash) = Algorithms::default()
82            .read_sync(data)
83            .context("failed to compute content digest")?;
84        if n != data.len() as u64 {
85            bail!(
86                "invalid amount of bytes read, expected: {}, got {n}",
87                data.len(),
88            )
89        }
90        let res = self
91            .create_request(&hash, mime)?
92            .send_bytes(data)
93            .map_err(parse_ureq_error)?;
94        match StatusCode::from_u16(res.status()) {
95            Ok(StatusCode::CREATED) => Ok(true),
96            Ok(StatusCode::OK) => Ok(false),
97            _ => bail!("unexpected status code: {}", res.status()),
98        }
99    }
100
101    pub(super) fn create_json(&self, mime: &Mime, val: &impl Serialize) -> Result<bool> {
102        let buf = serde_json::to_vec(val).context("failed to encode value to JSON")?;
103        self.create_bytes(mime, buf)
104    }
105
106    pub(super) fn create_from(
107        &self,
108        Meta { hash, size, mime }: &Meta,
109        rdr: impl Read,
110    ) -> Result<bool> {
111        let res = self
112            .create_request(hash, mime)?
113            .set(CONTENT_LENGTH.as_str(), &size.to_string())
114            .send(rdr)
115            .map_err(parse_ureq_error)?;
116        match StatusCode::from_u16(res.status()) {
117            Ok(StatusCode::CREATED) => Ok(true),
118            Ok(StatusCode::OK) => Ok(false),
119            _ => bail!("unexpected status code: {}", res.status()),
120        }
121    }
122
123    pub fn get(&self) -> Result<(u64, Mime, impl Read)> {
124        let url = self.client.url(&self.path)?;
125        let mut req = self.client.inner.get(url.as_str());
126        if let Some(ref token) = self.client.token {
127            req = req.set("Authorization", &format!("Bearer {token}"))
128        }
129        let res = req
130            .call()
131            .map_err(parse_ureq_error)
132            .context("GET request failed")?;
133
134        let hash: ContentDigest = parse_header(&res, "Content-Digest")?;
135        let mime = parse_header(&res, CONTENT_TYPE.as_str())?;
136        let size = parse_header(&res, CONTENT_LENGTH.as_str())?;
137        match StatusCode::from_u16(res.status()) {
138            Ok(StatusCode::OK) => Ok((size, mime, hash.verifier(res.into_reader().take(size)))),
139            _ => bail!("unexpected status code: {}", res.status()),
140        }
141    }
142
143    pub fn get_to(&self, dst: &mut impl Write) -> Result<(u64, Mime)> {
144        let (size, mime, mut rdr) = self.get()?;
145        let n = copy(&mut rdr, dst)?;
146        if n != size {
147            bail!(
148                "invalid amount of bytes read, expected {}, read {}",
149                size,
150                n,
151            )
152        }
153        Ok((size, mime))
154    }
155
156    pub fn get_json<T>(&self) -> Result<T>
157    where
158        for<'de> T: Deserialize<'de>,
159    {
160        let (_, _, rdr) = self.get()?;
161        serde_json::from_reader(rdr).context("failed to decode JSON")
162    }
163
164    pub fn get_bytes(&self) -> Result<(Mime, Vec<u8>)> {
165        let (size, mime, mut rdr) = self.get()?;
166        let mut buf =
167            Vec::with_capacity(size.try_into().context("failed to convert u64 to usize")?);
168        let n = copy(&mut rdr, &mut buf).context("I/O failure")?;
169        if n != size {
170            bail!(
171                "invalid amount of bytes read, expected {}, read {}",
172                size,
173                n,
174            )
175        };
176        Ok((mime, buf))
177    }
178
179    pub fn get_string(&self) -> Result<(Mime, String)> {
180        let (size, mime, mut rdr) = self.get()?;
181        let size = size.try_into().context("failed to convert u64 to usize")?;
182        let mut s = String::with_capacity(size);
183        let n = rdr.read_to_string(&mut s).context("I/O failure")?;
184        if n != size {
185            bail!(
186                "invalid amount of bytes read, expected {}, read {}",
187                size,
188                n,
189            )
190        };
191        Ok((mime, s))
192    }
193}