1use std::time::Duration;
5
6use log::{debug, error, warn};
7use reqwest::{Client, ClientBuilder, Response, Url};
8use serde::{de::DeserializeOwned, Serialize};
9
10use crate::auth::{ApiErrorResponse, AuthErrorResponse};
11use crate::errors::{NetDiskError, NetDiskResult};
12
13#[derive(Debug, Clone)]
17pub struct HttpClientConfig {
18 pub timeout: Duration,
20 pub connect_timeout: Duration,
22 pub max_retries: usize,
24 pub retry_delay_ms: u64,
26 pub user_agent: String,
28 pub follow_redirects: bool,
30 pub max_redirects: usize,
32}
33
34impl Default for HttpClientConfig {
35 fn default() -> Self {
36 HttpClientConfig {
37 timeout: Duration::from_secs(30),
38 connect_timeout: Duration::from_secs(10),
39 max_retries: 3,
40 retry_delay_ms: 1000,
41 user_agent: "pan.baidu.com".to_string(),
42 follow_redirects: true,
43 max_redirects: 10,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
53pub struct HttpClient {
54 inner: Client,
55 config: HttpClientConfig,
56 base_url: Url,
57}
58
59impl HttpClient {
60 pub fn new(config: HttpClientConfig) -> NetDiskResult<Self> {
74 let redirect_policy = if config.follow_redirects {
75 reqwest::redirect::Policy::limited(config.max_redirects)
76 } else {
77 reqwest::redirect::Policy::none()
78 };
79
80 let client = ClientBuilder::new()
81 .timeout(config.timeout)
82 .connect_timeout(config.connect_timeout)
83 .redirect(redirect_policy)
84 .user_agent("pan.baidu.com")
85 .build()
86 .map_err(|e| NetDiskError::Unknown {
87 message: format!("Failed to build HTTP client: {}", e),
88 })?;
89
90 let base_url = Url::parse("https://pan.baidu.com").map_err(|e| NetDiskError::Unknown {
91 message: format!("Failed to parse base URL: {}", e),
92 })?;
93
94 Ok(HttpClient {
95 inner: client,
96 config,
97 base_url,
98 })
99 }
100
101 pub fn try_default() -> NetDiskResult<Self> {
114 Self::new(HttpClientConfig::default())
115 }
116
117 pub async fn get<T: DeserializeOwned>(
137 &self,
138 url: &str,
139 params: Option<&[(&str, &str)]>,
140 ) -> NetDiskResult<T> {
141 self.get_with_headers(url, params, None).await
142 }
143
144 pub async fn get_with_headers<T: DeserializeOwned>(
146 &self,
147 url: &str,
148 params: Option<&[(&str, &str)]>,
149 headers: Option<&[(&str, &str)]>,
150 ) -> NetDiskResult<T> {
151 let url = if url.starts_with("http") {
152 Url::parse(url)?
153 } else {
154 self.build_url(url, params.unwrap_or(&[]))?
155 };
156
157 debug!("HTTP GET: {}", url);
158 if let Some(p) = params {
159 if !p.is_empty() {
160 debug!(" Query params: {:?}", p);
161 }
162 }
163 if let Some(h) = headers {
164 if !h.is_empty() {
165 debug!(" Headers: {:?}", h);
166 }
167 }
168
169 self.execute_request_with_retry(|| async {
170 let mut request = self.inner.get(url.clone());
171 if let Some(h) = headers {
172 for (key, value) in h.iter() {
173 request = request.header(*key, *value);
174 }
175 }
176 request.send().await
177 })
178 .await
179 }
180
181 pub async fn post_form<T: DeserializeOwned>(
183 &self,
184 url: &str,
185 form: Option<&[(&str, &str)]>,
186 params: Option<&[(&str, &str)]>,
187 ) -> NetDiskResult<T> {
188 let url = if url.starts_with("http") {
189 Url::parse(url)?
190 } else {
191 self.build_url(url, params.unwrap_or(&[]))?
192 };
193
194 let form = form.unwrap_or(&[]);
195 debug!("HTTP POST Form: {}", url);
196 if !form.is_empty() {
197 debug!(" Form data: {:?}", form);
198 }
199
200 self.execute_request_with_retry(|| async {
201 self.inner.post(url.clone()).form(form).send().await
202 })
203 .await
204 }
205
206 pub async fn post<T: DeserializeOwned>(
208 &self,
209 url: &str,
210 params: Option<&[(&str, &str)]>,
211 ) -> NetDiskResult<T> {
212 let url = if url.starts_with("http") {
213 Url::parse(url)?
214 } else {
215 self.build_url(url, params.unwrap_or(&[]))?
216 };
217
218 debug!("HTTP POST: {}", url);
219 if let Some(p) = params {
220 if !p.is_empty() {
221 debug!(" Query params: {:?}", p);
222 }
223 }
224
225 self.execute_request_with_retry(|| async { self.inner.post(url.clone()).send().await })
226 .await
227 }
228
229 pub async fn post_json<T: DeserializeOwned, U: Serialize + ?Sized>(
231 &self,
232 url: &str,
233 body: &U,
234 ) -> NetDiskResult<T> {
235 let url = if url.starts_with("http") {
236 Url::parse(url)?
237 } else {
238 self.build_url(url, &[])?
239 };
240
241 let json_body =
242 serde_json::to_string(body).unwrap_or_else(|_| "serialization failed".to_string());
243 debug!("HTTP POST JSON: {}", url);
244 debug!(" Body: {}", json_body);
245
246 self.execute_request_with_retry(|| async {
247 self.inner.post(url.clone()).json(body).send().await
248 })
249 .await
250 }
251
252 pub async fn post_multipart<T: DeserializeOwned>(
254 &self,
255 url: &str,
256 field_name: String,
257 file_name: String,
258 data: Vec<u8>,
259 ) -> NetDiskResult<T> {
260 let url = Url::parse(url)?;
261 debug!("HTTP POST Multipart: {}", url);
262 debug!(
263 " Field: {}, File: {}, Size: {} bytes",
264 field_name,
265 file_name,
266 data.len()
267 );
268
269 self.execute_request_with_retry(|| async {
270 let form = reqwest::multipart::Form::new().part(
271 field_name.clone(),
272 reqwest::multipart::Part::bytes(data.clone()).file_name(file_name.clone()),
273 );
274 self.inner.post(url.clone()).multipart(form).send().await
275 })
276 .await
277 }
278
279 fn build_url(&self, path: &str, params: &[(&str, &str)]) -> NetDiskResult<Url> {
281 let mut url = self.base_url.join(path)?;
282
283 if !params.is_empty() {
284 let mut pairs = url.query_pairs_mut();
285 for (key, value) in params {
286 pairs.append_pair(key, value);
287 }
288 }
289
290 debug!("Built URL: {}", url);
291 Ok(url)
292 }
293
294 async fn execute_request_with_retry<T: DeserializeOwned, F, Fut>(
296 &self,
297 make_request: F,
298 ) -> NetDiskResult<T>
299 where
300 F: Fn() -> Fut,
301 Fut: std::future::Future<Output = Result<Response, reqwest::Error>>,
302 {
303 let mut attempts = 0;
304
305 loop {
306 attempts += 1;
307
308 match make_request().await {
309 Ok(response) => {
310 if response.status().is_server_error() && attempts < self.config.max_retries {
311 warn!(
312 "Server error ({}), attempt {}/{}, retrying...",
313 response.status(),
314 attempts,
315 self.config.max_retries
316 );
317 tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
318 continue;
319 }
320 return self.parse_response(response).await;
321 }
322 Err(e) => {
323 if attempts < self.config.max_retries && self.should_retry(&e) {
324 warn!(
325 "Request failed, attempt {}/{}, retrying...: {}",
326 attempts, self.config.max_retries, e
327 );
328 tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
329 continue;
330 }
331 return Err(NetDiskError::http_error_with_source(0, "unknown", e));
332 }
333 }
334 }
335 }
336
337 fn should_retry(&self, err: &reqwest::Error) -> bool {
339 err.is_timeout() || err.is_connect() || err.is_body()
340 }
341
342 async fn parse_response<T: DeserializeOwned>(&self, response: Response) -> NetDiskResult<T> {
344 let status = response.status();
345 let url = response.url().to_string();
346
347 if status.is_success() {
348 let body = response.text().await.map_err(|e| NetDiskError::Unknown {
349 message: format!("Failed to read response body: {}", e),
350 })?;
351
352 debug!("Response body (status {}): {}", status, body);
353
354 match serde_json::from_str(&body) {
355 Ok(data) => Ok(data),
356 Err(e) => {
357 error!("Failed to parse JSON response: {}", e);
358 error!("Response body that failed to parse: {}", body);
359 Err(NetDiskError::Unknown {
360 message: format!("Failed to parse JSON: {}", e),
361 })
362 }
363 }
364 } else {
365 let body = match response.text().await {
366 Ok(b) => b,
367 Err(_) => String::from("Unknown error"),
368 };
369
370 error!("API request failed: {} - {}", status, body);
371
372 if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&body) {
373 Err(NetDiskError::api_error(
374 api_error.get_errno(),
375 api_error.get_errmsg(),
376 ))
377 } else if let Ok(auth_error) = serde_json::from_str::<AuthErrorResponse>(&body) {
378 Err(NetDiskError::auth_error(&auth_error.error_description))
379 } else {
380 Err(NetDiskError::http_error(status.as_u16(), &url))
381 }
382 }
383 }
384}