torrust_index/web/api/client/v1/
http.rs

1use std::time::Duration;
2
3use reqwest::{multipart, Error};
4use serde::Serialize;
5
6use super::connection_info::ConnectionInfo;
7use super::responses::{BinaryResponse, TextResponse};
8
9pub type ReqwestQuery = Vec<ReqwestQueryParam>;
10pub type ReqwestQueryParam = (String, String);
11
12/// URL Query component
13#[derive(Default, Debug)]
14pub struct Query {
15    params: Vec<QueryParam>,
16}
17
18impl Query {
19    #[must_use]
20    pub fn empty() -> Self {
21        Self { params: vec![] }
22    }
23
24    #[must_use]
25    pub fn with_params(params: Vec<QueryParam>) -> Self {
26        Self { params }
27    }
28
29    pub fn add_param(&mut self, param: QueryParam) {
30        self.params.push(param);
31    }
32}
33
34impl From<Query> for ReqwestQuery {
35    fn from(url_search_params: Query) -> Self {
36        url_search_params
37            .params
38            .iter()
39            .map(|param| ReqwestQueryParam::from((*param).clone()))
40            .collect()
41    }
42}
43
44/// URL query param
45#[derive(Clone, Debug)]
46pub struct QueryParam {
47    name: String,
48    value: String,
49}
50
51impl QueryParam {
52    #[must_use]
53    pub fn new(name: &str, value: &str) -> Self {
54        Self {
55            name: name.to_string(),
56            value: value.to_string(),
57        }
58    }
59}
60
61impl From<QueryParam> for ReqwestQueryParam {
62    fn from(param: QueryParam) -> Self {
63        (param.name, param.value)
64    }
65}
66
67/// Generic HTTP Client
68pub struct Http {
69    connection_info: ConnectionInfo,
70    /// The timeout is applied from when the request starts connecting until the
71    /// response body has finished.
72    timeout: Duration,
73}
74
75impl Http {
76    #[must_use]
77    pub fn new(connection_info: ConnectionInfo) -> Self {
78        Self {
79            connection_info,
80            timeout: Duration::from_secs(5),
81        }
82    }
83
84    /// # Errors
85    ///
86    /// Will return an error if there was an error while sending request,
87    /// redirect loop was detected or redirect limit was exhausted.
88    pub async fn get(&self, path: &str, params: Query) -> Result<TextResponse, Error> {
89        let response = match &self.connection_info.token {
90            Some(token) => {
91                reqwest::Client::builder()
92                    .timeout(self.timeout)
93                    .build()?
94                    .get(self.base_url(path).clone())
95                    .query(&ReqwestQuery::from(params))
96                    .bearer_auth(token)
97                    .send()
98                    .await?
99            }
100            None => {
101                reqwest::Client::builder()
102                    .timeout(self.timeout)
103                    .build()?
104                    .get(self.base_url(path).clone())
105                    .query(&ReqwestQuery::from(params))
106                    .send()
107                    .await?
108            }
109        };
110
111        Ok(TextResponse::from(response).await)
112    }
113
114    /// # Errors
115    ///
116    /// Will return an error if there was an error while sending request,
117    /// redirect loop was detected or redirect limit was exhausted.
118    pub async fn get_binary(&self, path: &str, params: Query) -> Result<BinaryResponse, Error> {
119        let response = match &self.connection_info.token {
120            Some(token) => {
121                reqwest::Client::builder()
122                    .timeout(self.timeout)
123                    .build()?
124                    .get(self.base_url(path).clone())
125                    .query(&ReqwestQuery::from(params))
126                    .bearer_auth(token)
127                    .send()
128                    .await?
129            }
130            None => {
131                reqwest::Client::builder()
132                    .timeout(self.timeout)
133                    .build()?
134                    .get(self.base_url(path).clone())
135                    .query(&ReqwestQuery::from(params))
136                    .send()
137                    .await?
138            }
139        };
140
141        // todo: If the response is a JSON, it returns the JSON body in a byte
142        //   array. This is not the expected behavior.
143        //  - Rename BinaryResponse to BinaryTorrentResponse
144        //  - Return an error if the response is not a bittorrent file
145        Ok(BinaryResponse::from(response).await)
146    }
147
148    /// # Errors
149    ///
150    /// Will return an error if there was an error while sending request,
151    /// redirect loop was detected or redirect limit was exhausted.
152    pub async fn inner_get(&self, path: &str) -> Result<reqwest::Response, reqwest::Error> {
153        reqwest::Client::builder()
154            .timeout(self.timeout)
155            .build()?
156            .get(self.base_url(path).clone())
157            .send()
158            .await
159    }
160
161    /// # Errors
162    ///
163    /// Will return an error if there was an error while sending request,
164    /// redirect loop was detected or redirect limit was exhausted.
165    pub async fn post<T: Serialize + ?Sized>(&self, path: &str, form: &T) -> Result<TextResponse, reqwest::Error> {
166        let response = match &self.connection_info.token {
167            Some(token) => {
168                reqwest::Client::new()
169                    .post(self.base_url(path).clone())
170                    .bearer_auth(token)
171                    .json(&form)
172                    .send()
173                    .await?
174            }
175            None => {
176                reqwest::Client::new()
177                    .post(self.base_url(path).clone())
178                    .json(&form)
179                    .send()
180                    .await?
181            }
182        };
183
184        Ok(TextResponse::from(response).await)
185    }
186
187    /// # Errors
188    ///
189    /// Will return an error if there was an error while sending request,
190    /// redirect loop was detected or redirect limit was exhausted.
191    pub async fn post_multipart(&self, path: &str, form: multipart::Form) -> Result<TextResponse, reqwest::Error> {
192        let response = match &self.connection_info.token {
193            Some(token) => {
194                reqwest::Client::builder()
195                    .timeout(self.timeout)
196                    .build()?
197                    .post(self.base_url(path).clone())
198                    .multipart(form)
199                    .bearer_auth(token)
200                    .send()
201                    .await?
202            }
203            None => {
204                reqwest::Client::builder()
205                    .timeout(self.timeout)
206                    .build()?
207                    .post(self.base_url(path).clone())
208                    .multipart(form)
209                    .send()
210                    .await?
211            }
212        };
213
214        Ok(TextResponse::from(response).await)
215    }
216
217    /// # Errors
218    ///
219    /// Will return an error if there was an error while sending request,
220    /// redirect loop was detected or redirect limit was exhausted.
221    pub async fn put<T: Serialize + ?Sized>(&self, path: &str, form: &T) -> Result<TextResponse, reqwest::Error> {
222        let response = match &self.connection_info.token {
223            Some(token) => {
224                reqwest::Client::new()
225                    .put(self.base_url(path).clone())
226                    .bearer_auth(token)
227                    .json(&form)
228                    .send()
229                    .await?
230            }
231            None => {
232                reqwest::Client::new()
233                    .put(self.base_url(path).clone())
234                    .json(&form)
235                    .send()
236                    .await?
237            }
238        };
239
240        Ok(TextResponse::from(response).await)
241    }
242
243    /// # Errors
244    ///
245    /// Will return an error if there was an error while sending request,
246    /// redirect loop was detected or redirect limit was exhausted.   
247    pub async fn delete(&self, path: &str) -> Result<TextResponse, reqwest::Error> {
248        let response = match &self.connection_info.token {
249            Some(token) => {
250                reqwest::Client::new()
251                    .delete(self.base_url(path).clone())
252                    .bearer_auth(token)
253                    .send()
254                    .await?
255            }
256            None => reqwest::Client::new().delete(self.base_url(path).clone()).send().await?,
257        };
258
259        Ok(TextResponse::from(response).await)
260    }
261
262    /// # Errors
263    ///
264    /// Will return an error if there was an error while sending request,
265    /// redirect loop was detected or redirect limit was exhausted.
266    pub async fn delete_with_body<T: Serialize + ?Sized>(&self, path: &str, form: &T) -> Result<TextResponse, reqwest::Error> {
267        let response = match &self.connection_info.token {
268            Some(token) => {
269                reqwest::Client::new()
270                    .delete(self.base_url(path).clone())
271                    .bearer_auth(token)
272                    .json(&form)
273                    .send()
274                    .await?
275            }
276            None => {
277                reqwest::Client::new()
278                    .delete(self.base_url(path).clone())
279                    .json(&form)
280                    .send()
281                    .await?
282            }
283        };
284
285        Ok(TextResponse::from(response).await)
286    }
287
288    fn base_url(&self, path: &str) -> String {
289        format!(
290            "{}://{}{}{path}",
291            &self.connection_info.scheme, &self.connection_info.bind_address, &self.connection_info.base_path
292        )
293    }
294}