noosphere_ipfs/client/
gateway.rs

1use super::{IpfsClient, IpfsClientAsyncReadSendSync};
2use anyhow::Result;
3use async_trait::async_trait;
4use cid::Cid;
5use noosphere_common::ConditionalSend;
6use reqwest::Client;
7use reqwest::StatusCode;
8use std::str::FromStr;
9use url::Url;
10
11/// A high-level HTTP client for accessing IPFS
12/// [HTTP Gateway](https://docs.ipfs.tech/reference/http/gateway/) and normalizing
13/// their expected payloads to Noosphere-friendly formats.
14#[derive(Clone, Debug)]
15pub struct GatewayClient {
16    client: Client,
17    api_url: Url,
18}
19
20impl GatewayClient {
21    pub fn new(api_url: Url) -> Self {
22        let client = Client::new();
23        GatewayClient { client, api_url }
24    }
25
26    pub(crate) fn make_block_url(&self, cid: &Cid) -> Url {
27        let mut url = self.api_url.clone();
28
29        if let Some(domain) = url.domain() {
30            let mut parts = domain.split('.');
31
32            if let Some(fragment) = parts.next() {
33                if Cid::from_str(fragment).is_ok() {
34                    let upper_domain = parts
35                        .map(|part| part.to_string())
36                        .collect::<Vec<String>>()
37                        .join(".");
38
39                    let mut host = format!("{cid}.{upper_domain}");
40
41                    if let Some(port) = url.port() {
42                        host = format!("{host}:{port}");
43                    }
44
45                    if let Ok(()) = url.set_host(Some(&host)) {
46                        return url;
47                    }
48                }
49            }
50        }
51
52        url.set_path(&format!("/ipfs/{cid}"));
53        url
54    }
55}
56
57#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
58#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
59impl IpfsClient for GatewayClient {
60    async fn pin_blocks<'a, I>(&self, _cids: I) -> Result<()>
61    where
62        I: IntoIterator<Item = &'a Cid> + ConditionalSend + std::fmt::Debug,
63        I::IntoIter: ConditionalSend,
64    {
65        unimplemented!("IPFS HTTP Gateway does not have this capability.");
66    }
67
68    async fn block_is_pinned(&self, _cid: &Cid) -> Result<bool> {
69        unimplemented!("IPFS HTTP Gateway does not have this capability.");
70    }
71
72    async fn server_identity(&self) -> Result<String> {
73        unimplemented!("IPFS HTTP Gateway does not have this capability.");
74    }
75
76    async fn syndicate_blocks<R>(&self, _car: R) -> Result<()>
77    where
78        R: IpfsClientAsyncReadSendSync,
79    {
80        unimplemented!("IPFS HTTP Gateway does not have this capability.");
81    }
82
83    async fn put_block(&mut self, _cid: &Cid, _block: &[u8]) -> Result<()> {
84        unimplemented!("IPFS HTTP Gateway does not have this capability.");
85    }
86
87    async fn get_block(&self, cid: &Cid) -> Result<Option<Vec<u8>>> {
88        let api_url = self.make_block_url(cid);
89        let response = self
90            .client
91            .get(api_url)
92            .header("Accept", "application/vnd.ipld.raw")
93            .send()
94            .await?;
95
96        match response.status() {
97            StatusCode::OK => Ok(Some(response.bytes().await?.into())),
98            _ => Ok(None),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn it_can_derive_a_block_url_for_subdomain_gateways() {
109        let gateway_url = Url::from_str(
110            "https://bafybeieh53mh2gt4khnrixfro7wvbvtrux4247cfwse642e36z67medkzq.ipfs.noo.pub",
111        )
112        .unwrap();
113        let test_cid =
114            Cid::from_str("bafy2bzacecsjls67zqx25dcvbu6p4z4rsdkm2k6hanhd5qowrvwmhtov2sjpo")
115                .unwrap();
116        let client = GatewayClient::new(gateway_url);
117        let derived_url = client.make_block_url(&test_cid);
118        let expected_url = Url::from_str(
119            "https://bafy2bzacecsjls67zqx25dcvbu6p4z4rsdkm2k6hanhd5qowrvwmhtov2sjpo.ipfs.noo.pub",
120        )
121        .unwrap();
122
123        assert_eq!(derived_url, expected_url);
124    }
125}