1use crate::error::{IoError, Result};
7use crate::network::NetworkConfig;
8use std::collections::HashMap;
9use std::path::Path;
10use std::time::Duration;
11#[cfg(feature = "reqwest")]
12use std::time::Instant;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum HttpMethod {
17 GET,
19 POST,
21 PUT,
23 DELETE,
25 HEAD,
27}
28
29impl std::fmt::Display for HttpMethod {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 HttpMethod::GET => write!(f, "GET"),
33 HttpMethod::POST => write!(f, "POST"),
34 HttpMethod::PUT => write!(f, "PUT"),
35 HttpMethod::DELETE => write!(f, "DELETE"),
36 HttpMethod::HEAD => write!(f, "HEAD"),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct HttpResponse {
44 pub status: u16,
46 pub headers: HashMap<String, String>,
48 pub content_length: Option<u64>,
50 pub content_type: Option<String>,
52 pub body: Vec<u8>,
54}
55
56pub type ProgressCallback = Box<dyn Fn(u64, Option<u64>) + Send + Sync>;
58
59#[derive(Debug)]
76pub struct HttpClient {
77 #[allow(dead_code)]
78 config: NetworkConfig,
79 #[cfg(feature = "reqwest")]
80 client: Option<reqwest::Client>,
81}
82
83impl HttpClient {
84 pub fn new(config: NetworkConfig) -> Self {
86 let mut client = Self {
87 config,
88 #[cfg(feature = "reqwest")]
89 client: None,
90 };
91
92 #[cfg(feature = "reqwest")]
94 {
95 let _ = client.init();
96 }
97
98 client
99 }
100
101 #[cfg(feature = "reqwest")]
103 pub fn init(&mut self) -> Result<()> {
104 crate::tls::ensure_default_tls_provider();
105 let mut client_builder = reqwest::Client::builder()
106 .connect_timeout(self.config.connect_timeout)
107 .timeout(self.config.read_timeout)
108 .user_agent(&self.config.user_agent);
109
110 let mut headers = reqwest::header::HeaderMap::new();
112 for (key, value) in &self.config.headers {
113 if let (Ok(header_name), Ok(header_value)) = (
114 reqwest::header::HeaderName::from_bytes(key.as_bytes()),
115 reqwest::header::HeaderValue::from_str(value),
116 ) {
117 headers.insert(header_name, header_value);
118 }
119 }
120 client_builder = client_builder.default_headers(headers);
121
122 self.client =
123 Some(client_builder.build().map_err(|e| {
124 IoError::NetworkError(format!("Failed to create HTTP client: {}", e))
125 })?);
126
127 Ok(())
128 }
129
130 #[cfg(all(feature = "reqwest", feature = "async"))]
132 pub async fn download<P: AsRef<Path>>(&self, url: &str, localpath: P) -> Result<()> {
133 let client = self.get_client()?;
134 let local_path = localpath.as_ref();
135
136 if let Some(parent) = local_path.parent() {
138 std::fs::create_dir_all(parent)
139 .map_err(|e| IoError::FileError(format!("Failed to create directory: {}", e)))?;
140 }
141
142 let mut retries = 0;
143 loop {
144 let start_time = Instant::now();
145
146 match self.download_with_retry(client, url, local_path).await {
147 Ok(_) => {
148 let duration = start_time.elapsed();
149 log::info!("Downloaded {} in {:.2}s", url, duration.as_secs_f64());
150 return Ok(());
151 }
152 Err(e) => {
153 retries += 1;
154 if retries > self.config.max_retries {
155 return Err(e);
156 }
157
158 let delay = Duration::from_millis(100 * 2_u64.pow(retries - 1));
159 log::warn!(
160 "Download failed, retrying in {}ms: {}",
161 delay.as_millis(),
162 e
163 );
164 #[cfg(feature = "async")]
165 tokio::time::sleep(delay).await;
166 }
167 }
168 }
169 }
170
171 #[cfg(all(feature = "reqwest", feature = "async"))]
172 async fn download_with_retry(
173 &self,
174 client: &reqwest::Client,
175 url: &str,
176 local_path: &Path,
177 ) -> Result<()> {
178 let response = client
179 .get(url)
180 .send()
181 .await
182 .map_err(|e| IoError::NetworkError(format!("HTTP request failed: {}", e)))?;
183
184 if !response.status().is_success() {
185 return Err(IoError::NetworkError(format!(
186 "HTTP error {}: {}",
187 response.status().as_u16(),
188 response
189 .status()
190 .canonical_reason()
191 .unwrap_or("Unknown error")
192 )));
193 }
194
195 let content_length = response.content_length();
196
197 let mut file = std::fs::File::create(local_path)
198 .map_err(|e| IoError::FileError(format!("Failed to create file: {}", e)))?;
199
200 use std::io::Write;
201
202 let bytes = response
203 .bytes()
204 .await
205 .map_err(|e| IoError::NetworkError(format!("Failed to read response body: {}", e)))?;
206
207 file.write_all(&bytes)
208 .map_err(|e| IoError::FileError(format!("Failed to write file: {}", e)))?;
209
210 let downloaded = bytes.len() as u64;
211
212 if let Some(total) = content_length {
214 let progress = (downloaded as f64 / total as f64 * 100.0) as u8;
215 log::debug!("Download progress: {}%", progress);
216 }
217
218 Ok(())
219 }
220
221 #[cfg(all(feature = "reqwest", feature = "async"))]
223 pub async fn upload<P: AsRef<Path>>(&self, localpath: P, url: &str) -> Result<()> {
224 let client = self.get_client()?;
225 let local_path = localpath.as_ref();
226
227 if !local_path.exists() {
228 return Err(IoError::FileError(format!(
229 "File does not exist: {}",
230 local_path.display()
231 )));
232 }
233
234 let file_content = std::fs::read(local_path)
235 .map_err(|e| IoError::FileError(format!("Failed to read file: {}", e)))?;
236
237 let mut retries = 0;
238 loop {
239 let start_time = Instant::now();
240
241 match self.upload_with_retry(client, &file_content, url).await {
242 Ok(_) => {
243 let duration = start_time.elapsed();
244 log::info!(
245 "Uploaded {} in {:.2}s",
246 local_path.display(),
247 duration.as_secs_f64()
248 );
249 return Ok(());
250 }
251 Err(e) => {
252 retries += 1;
253 if retries > self.config.max_retries {
254 return Err(e);
255 }
256
257 let delay = Duration::from_millis(100 * 2_u64.pow(retries - 1));
258 log::warn!("Upload failed, retrying in {}ms: {}", delay.as_millis(), e);
259 #[cfg(feature = "async")]
260 tokio::time::sleep(delay).await;
261 }
262 }
263 }
264 }
265
266 #[cfg(all(feature = "reqwest", feature = "async"))]
267 async fn upload_with_retry(
268 &self,
269 client: &reqwest::Client,
270 content: &[u8],
271 url: &str,
272 ) -> Result<()> {
273 let response = client
274 .put(url)
275 .body(content.to_vec())
276 .send()
277 .await
278 .map_err(|e| IoError::NetworkError(format!("HTTP upload failed: {}", e)))?;
279
280 if !response.status().is_success() {
281 return Err(IoError::NetworkError(format!(
282 "HTTP upload error {}: {}",
283 response.status().as_u16(),
284 response
285 .status()
286 .canonical_reason()
287 .unwrap_or("Unknown error")
288 )));
289 }
290
291 Ok(())
292 }
293
294 #[cfg(all(feature = "reqwest", feature = "async"))]
296 pub async fn request(
297 &self,
298 method: HttpMethod,
299 url: &str,
300 body: Option<&[u8]>,
301 ) -> Result<HttpResponse> {
302 let client = self.get_client()?;
303
304 let mut request_builder = match method {
305 HttpMethod::GET => client.get(url),
306 HttpMethod::POST => client.post(url),
307 HttpMethod::PUT => client.put(url),
308 HttpMethod::DELETE => client.delete(url),
309 HttpMethod::HEAD => client.head(url),
310 };
311
312 if let Some(body_data) = body {
313 request_builder = request_builder.body(body_data.to_vec());
314 }
315
316 let response = request_builder
317 .send()
318 .await
319 .map_err(|e| IoError::NetworkError(format!("HTTP request failed: {}", e)))?;
320
321 let status = response.status().as_u16();
322 let headers = response
323 .headers()
324 .iter()
325 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
326 .collect();
327
328 let content_length = response.content_length();
329 let content_type = response
330 .headers()
331 .get(reqwest::header::CONTENT_TYPE)
332 .and_then(|v| v.to_str().ok())
333 .map(|s| s.to_string());
334
335 let body = response
336 .bytes()
337 .await
338 .map_err(|e| IoError::NetworkError(format!("Failed to read response body: {}", e)))?
339 .to_vec();
340
341 Ok(HttpResponse {
342 status,
343 headers,
344 content_length,
345 content_type,
346 body,
347 })
348 }
349
350 #[cfg(all(feature = "reqwest", feature = "async"))]
352 pub async fn check_url(&self, url: &str) -> Result<bool> {
353 match self.request(HttpMethod::HEAD, url, None).await {
354 Ok(response) => Ok(response.status >= 200 && response.status < 300),
355 Err(_) => Ok(false),
356 }
357 }
358
359 #[cfg(all(feature = "reqwest", feature = "async"))]
361 pub async fn get_remote_file_size(&self, url: &str) -> Result<Option<u64>> {
362 let response = self.request(HttpMethod::HEAD, url, None).await?;
363 Ok(response.content_length)
364 }
365
366 #[cfg(feature = "reqwest")]
367 fn get_client(&self) -> Result<&reqwest::Client> {
368 self.client
369 .as_ref()
370 .ok_or_else(|| IoError::ConfigError("HTTP client not initialized".to_string()))
371 }
372
373 #[cfg(not(feature = "reqwest"))]
375 pub async fn download<P: AsRef<Path>>(url: &str, _localpath: P) -> Result<()> {
377 Err(IoError::ConfigError(
378 "HTTP support requires 'reqwest' feature".to_string(),
379 ))
380 }
381
382 #[cfg(not(feature = "reqwest"))]
383 pub async fn upload<P: AsRef<Path>>(_local_path: P, path: P, url: &str) -> Result<()> {
385 Err(IoError::ConfigError(
386 "HTTP support requires 'reqwest' feature".to_string(),
387 ))
388 }
389
390 #[cfg(not(feature = "reqwest"))]
391 pub async fn request(
393 &self,
394 _method: HttpMethod,
395 _url: &str,
396 _body: Option<&[u8]>,
397 ) -> Result<HttpResponse> {
398 Err(IoError::ConfigError(
399 "HTTP support requires 'reqwest' feature".to_string(),
400 ))
401 }
402
403 #[cfg(not(feature = "reqwest"))]
404 pub async fn check_url(url: &str) -> Result<bool> {
406 Err(IoError::ConfigError(
407 "HTTP support requires 'reqwest' feature".to_string(),
408 ))
409 }
410
411 #[cfg(not(feature = "reqwest"))]
412 pub async fn get_remote_file_size(url: &str) -> Result<Option<u64>> {
414 Err(IoError::ConfigError(
415 "HTTP support requires 'reqwest' feature".to_string(),
416 ))
417 }
418}
419
420#[cfg(all(feature = "reqwest", feature = "async"))]
423pub async fn download_concurrent(
424 downloads: Vec<(String, String)>,
425 max_concurrent: usize,
426) -> Result<Vec<Result<()>>> {
427 use futures_util::stream::{FuturesUnordered, StreamExt};
428
429 let client = HttpClient::new(NetworkConfig::default());
430
431 #[cfg(feature = "async")]
432 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(max_concurrent));
433 let mut futures = FuturesUnordered::new();
434
435 for (url, local_path) in downloads {
436 let client_clone = &client;
437 let semaphore_clone = semaphore.clone();
438
439 futures.push(async move {
440 #[cfg(feature = "async")]
441 let _permit = semaphore_clone.acquire().await.expect("Operation failed");
442 client_clone.download(&url, &local_path).await
443 });
444 }
445
446 let mut results = Vec::new();
447 while let Some(result) = futures.next().await {
448 results.push(result);
449 }
450
451 Ok(results)
452}
453
454#[allow(dead_code)]
456pub fn calculate_speed(bytes: u64, duration: Duration) -> f64 {
457 if duration.as_secs_f64() > 0.0 {
458 bytes as f64 / duration.as_secs_f64()
459 } else {
460 0.0
461 }
462}
463
464#[allow(dead_code)]
466pub fn format_file_size(bytes: u64) -> String {
467 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
468 let mut size = bytes as f64;
469 let mut unit_index = 0;
470
471 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
472 size /= 1024.0;
473 unit_index += 1;
474 }
475
476 format!("{:.1} {}", size, UNITS[unit_index])
477}
478
479#[allow(dead_code)]
481pub fn format_speed(bytes_per_second: f64) -> String {
482 format!("{}/s", format_file_size(bytes_per_second as u64))
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_http_method_display() {
491 assert_eq!(HttpMethod::GET.to_string(), "GET");
492 assert_eq!(HttpMethod::POST.to_string(), "POST");
493 assert_eq!(HttpMethod::PUT.to_string(), "PUT");
494 assert_eq!(HttpMethod::DELETE.to_string(), "DELETE");
495 assert_eq!(HttpMethod::HEAD.to_string(), "HEAD");
496 }
497
498 #[test]
499 fn test_http_response_creation() {
500 let mut headers = HashMap::new();
501 headers.insert("content-type".to_string(), "application/json".to_string());
502
503 let response = HttpResponse {
504 status: 200,
505 headers,
506 content_length: Some(1024),
507 content_type: Some("application/json".to_string()),
508 body: b"test data".to_vec(),
509 };
510
511 assert_eq!(response.status, 200);
512 assert_eq!(response.content_length, Some(1024));
513 assert_eq!(response.content_type, Some("application/json".to_string()));
514 assert_eq!(response.body, b"test data");
515 }
516
517 #[test]
518 fn test_http_client_creation() {
519 let config = NetworkConfig::default();
520 let _client = HttpClient::new(config);
521
522 }
525
526 #[test]
527 fn test_format_file_size() {
528 assert_eq!(format_file_size(512), "512.0 B");
529 assert_eq!(format_file_size(1024), "1.0 KB");
530 assert_eq!(format_file_size(1536), "1.5 KB");
531 assert_eq!(format_file_size(1024 * 1024), "1.0 MB");
532 assert_eq!(format_file_size(1024 * 1024 * 1024), "1.0 GB");
533 }
534
535 #[test]
536 fn test_calculate_speed() {
537 let duration = Duration::from_secs(1);
538 assert_eq!(calculate_speed(1024, duration), 1024.0);
539
540 let duration = Duration::from_secs(2);
541 assert_eq!(calculate_speed(2048, duration), 1024.0);
542
543 let duration = Duration::from_secs(0);
544 assert_eq!(calculate_speed(1024, duration), 0.0);
545 }
546
547 #[test]
548 fn test_format_speed() {
549 assert_eq!(format_speed(1024.0), "1.0 KB/s");
550 assert_eq!(format_speed(1024.0 * 1024.0), "1.0 MB/s");
551 }
552
553 #[cfg(feature = "async")]
554 #[tokio::test]
555 async fn test_http_client_without_reqwest_feature() {
556 let config = NetworkConfig::default();
557 let client = HttpClient::new(config);
558
559 #[cfg(not(feature = "reqwest"))]
561 {
562 let download_result = client.download("http://example.com", "test.txt").await;
563 assert!(download_result.is_err());
564
565 let upload_result = client.upload("test.txt", "http://example.com").await;
566 assert!(upload_result.is_err());
567
568 let request_result = client
569 .request(HttpMethod::GET, "http://example.com", None)
570 .await;
571 assert!(request_result.is_err());
572
573 let check_result = client.check_url("http://example.com").await;
574 assert!(check_result.is_err());
575
576 let size_result = client.get_remote_file_size("http://example.com").await;
577 assert!(size_result.is_err());
578 }
579 }
580}