Skip to main content

bliss_net/
lib.rs

1//! Networking (HTTP, filesystem, Data URIs) for Bliss
2//!
3//! Provides an implementation of the [`bliss_traits::net::NetProvider`] trait.
4
5// use bliss_traits::net::{Body, Bytes, NetHandler, NetProvider, NetWaker, Request};
6use bliss_traits::net::{AbortSignal, Body, Bytes, NetHandler, NetProvider, NetWaker, Request};
7use data_url::DataUrl;
8use std::{marker::PhantomData, pin::Pin, sync::Arc, task::Poll};
9use tokio::runtime::Handle;
10
11#[cfg(feature = "cache")]
12use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
13
14const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0";
15
16#[cfg(feature = "cache")]
17type Client = reqwest_middleware::ClientWithMiddleware;
18#[cfg(not(feature = "cache"))]
19type Client = reqwest::Client;
20
21#[cfg(feature = "cache")]
22type RequestBuilder = reqwest_middleware::RequestBuilder;
23#[cfg(not(feature = "cache"))]
24type RequestBuilder = reqwest::RequestBuilder;
25
26#[cfg(feature = "cache")]
27fn get_cache_path() -> std::path::PathBuf {
28    use directories::ProjectDirs;
29    let path = ProjectDirs::from("com", "DioxusLabs", "Bliss")
30        .expect("Failed to find cache directory")
31        .cache_dir()
32        .to_owned();
33    println!("Using cache dir {}", path.display());
34    path
35}
36
37pub struct Provider {
38    rt: Handle,
39    client: Client,
40    waker: Arc<dyn NetWaker>,
41}
42impl Provider {
43    pub fn new(waker: Option<Arc<dyn NetWaker>>) -> Self {
44        let builder = reqwest::Client::builder();
45        #[cfg(feature = "cookies")]
46        let builder = builder.cookie_store(true);
47        let client = builder.build().unwrap();
48
49        #[cfg(feature = "cache")]
50        let client = reqwest_middleware::ClientBuilder::new(client)
51            .with(Cache(HttpCache {
52                mode: CacheMode::Default,
53                manager: CACacheManager::new(get_cache_path(), true),
54                options: HttpCacheOptions::default(),
55            }))
56            .build();
57
58        let waker = waker.unwrap_or(Arc::new(DummyNetWaker));
59        Self {
60            rt: Handle::current(),
61            client,
62            waker,
63        }
64    }
65    pub fn shared(waker: Option<Arc<dyn NetWaker>>) -> Arc<dyn NetProvider> {
66        Arc::new(Self::new(waker))
67    }
68    pub fn is_empty(&self) -> bool {
69        Arc::strong_count(&self.waker) == 1
70    }
71    pub fn count(&self) -> usize {
72        Arc::strong_count(&self.waker) - 1
73    }
74}
75impl Provider {
76    async fn fetch_inner(
77        client: Client,
78        request: Request,
79    ) -> Result<(String, Bytes), ProviderError> {
80        Ok(match request.url.scheme() {
81            "data" => {
82                let data_url = DataUrl::process(request.url.as_str())?;
83                let decoded = data_url.decode_to_vec()?;
84                (request.url.to_string(), Bytes::from(decoded.0))
85            }
86            "file" => {
87                let file_content = std::fs::read(request.url.path())?;
88                (request.url.to_string(), Bytes::from(file_content))
89            }
90            _ => {
91                let response = client
92                    .request(request.method, request.url)
93                    .headers(request.headers)
94                    .header("Content-Type", request.content_type.as_str())
95                    .header("User-Agent", USER_AGENT)
96                    .apply_body(request.body, request.content_type.as_str())
97                    .await
98                    .send()
99                    .await?;
100
101                (response.url().to_string(), response.bytes().await?)
102            }
103        })
104    }
105
106    #[allow(clippy::type_complexity)]
107    pub fn fetch_with_callback(
108        &self,
109        request: Request,
110        callback: Box<dyn FnOnce(Result<(String, Bytes), ProviderError>) + Send + Sync + 'static>,
111    ) {
112        #[cfg(feature = "debug_log")]
113        let url = request.url.to_string();
114
115        let client = self.client.clone();
116        self.rt.spawn(async move {
117            let result = Self::fetch_inner(client, request).await;
118
119            #[cfg(feature = "debug_log")]
120            if let Err(e) = &result {
121                eprintln!("Error fetching {url}: {e:?}");
122            } else {
123                println!("Success {url}");
124            }
125
126            callback(result);
127        });
128    }
129
130    pub async fn fetch_async(&self, request: Request) -> Result<(String, Bytes), ProviderError> {
131        #[cfg(feature = "debug_log")]
132        let url = request.url.to_string();
133
134        let client = self.client.clone();
135        let result = Self::fetch_inner(client, request).await;
136
137        #[cfg(feature = "debug_log")]
138        if let Err(e) = &result {
139            eprintln!("Error fetching {url}: {e:?}");
140        } else {
141            println!("Success {url}");
142        }
143
144        result
145    }
146}
147
148impl NetProvider for Provider {
149    fn fetch(&self, doc_id: usize, mut request: Request, handler: Box<dyn NetHandler>) {
150        let client = self.client.clone();
151
152        #[cfg(feature = "debug_log")]
153        println!("Fetching {}", &request.url);
154
155        let waker = self.waker.clone();
156        self.rt.spawn(async move {
157            #[cfg(feature = "debug_log")]
158            let url = request.url.to_string();
159
160            let signal = request.signal.take();
161            let result = if let Some(signal) = signal {
162                AbortFetch::new(
163                    signal,
164                    Box::pin(async move { Self::fetch_inner(client, request).await }),
165                )
166                .await
167            } else {
168                Self::fetch_inner(client, request).await
169            };
170
171            // Call the waker to notify of completed network request
172            waker.wake(doc_id);
173
174            match result {
175                Ok((response_url, bytes)) => {
176                    handler.bytes(response_url, bytes);
177                    #[cfg(feature = "debug_log")]
178                    println!("Success {url}");
179                }
180                Err(e) => {
181                    #[cfg(feature = "debug_log")]
182                    eprintln!("Error fetching {url}: {e:?}");
183                    #[cfg(not(feature = "debug_log"))]
184                    let _ = e;
185                }
186            };
187        });
188    }
189}
190
191/// A future that is cancellable using an AbortSignal
192struct AbortFetch<F, T> {
193    signal: AbortSignal,
194    future: F,
195    _rt: PhantomData<T>,
196}
197
198impl<F, T> AbortFetch<F, T> {
199    fn new(signal: AbortSignal, future: F) -> Self {
200        Self {
201            signal,
202            future,
203            _rt: PhantomData,
204        }
205    }
206}
207
208impl<F, T> Future for AbortFetch<F, T>
209where
210    F: Future + Unpin + Send + 'static,
211    F::Output: Send + Into<Result<T, ProviderError>> + 'static,
212    T: Unpin,
213{
214    type Output = Result<T, ProviderError>;
215
216    fn poll(
217        mut self: std::pin::Pin<&mut Self>,
218        cx: &mut std::task::Context<'_>,
219    ) -> std::task::Poll<Self::Output> {
220        if self.signal.aborted() {
221            return Poll::Ready(Err(ProviderError::Abort));
222        }
223
224        match Pin::new(&mut self.future).poll(cx) {
225            Poll::Ready(output) => Poll::Ready(output.into()),
226            Poll::Pending => Poll::Pending,
227        }
228    }
229}
230
231#[derive(Debug)]
232pub enum ProviderError {
233    Abort,
234    Io(std::io::Error),
235    DataUrl(data_url::DataUrlError),
236    DataUrlBase64(data_url::forgiving_base64::InvalidBase64),
237    ReqwestError(reqwest::Error),
238    #[cfg(feature = "cache")]
239    ReqwestMiddlewareError(reqwest_middleware::Error),
240}
241
242impl From<std::io::Error> for ProviderError {
243    fn from(value: std::io::Error) -> Self {
244        Self::Io(value)
245    }
246}
247
248impl From<data_url::DataUrlError> for ProviderError {
249    fn from(value: data_url::DataUrlError) -> Self {
250        Self::DataUrl(value)
251    }
252}
253
254impl From<data_url::forgiving_base64::InvalidBase64> for ProviderError {
255    fn from(value: data_url::forgiving_base64::InvalidBase64) -> Self {
256        Self::DataUrlBase64(value)
257    }
258}
259
260impl From<reqwest::Error> for ProviderError {
261    fn from(value: reqwest::Error) -> Self {
262        Self::ReqwestError(value)
263    }
264}
265
266#[cfg(feature = "cache")]
267impl From<reqwest_middleware::Error> for ProviderError {
268    fn from(value: reqwest_middleware::Error) -> Self {
269        Self::ReqwestMiddlewareError(value)
270    }
271}
272
273trait ReqwestExt {
274    async fn apply_body(self, body: Body, content_type: &str) -> Self;
275}
276impl ReqwestExt for RequestBuilder {
277    async fn apply_body(self, body: Body, content_type: &str) -> Self {
278        match body {
279            Body::Bytes(bytes) => self.body(bytes),
280            Body::Form(form_data) => match content_type {
281                "application/x-www-form-urlencoded" => self.form(&form_data),
282                #[cfg(feature = "multipart")]
283                "multipart/form-data" => {
284                    use bliss_traits::net::Entry;
285                    use bliss_traits::net::EntryValue;
286                    let mut form_data = form_data;
287                    let mut form = reqwest::multipart::Form::new();
288                    for Entry { name, value } in form_data.0.drain(..) {
289                        form = match value {
290                            EntryValue::String(value) => form.text(name, value),
291                            EntryValue::File(path_buf) => form
292                                .file(name, path_buf)
293                                .await
294                                .expect("Couldn't read form file from disk"),
295                            EntryValue::EmptyFile => form.part(
296                                name,
297                                reqwest::multipart::Part::bytes(&[])
298                                    .mime_str("application/octet-stream")
299                                    .unwrap(),
300                            ),
301                        };
302                    }
303                    self.multipart(form)
304                }
305                _ => self,
306            },
307            Body::Empty => self,
308        }
309    }
310}
311
312struct DummyNetWaker;
313impl NetWaker for DummyNetWaker {
314    fn wake(&self, _client_id: usize) {}
315}