1use std::time::Duration;
2use reqwest::header::{HeaderMap, RANGE};
3use reqwest::{Client, StatusCode};
4use url::Url;
5
6use crate::error::{HttpError, Result};
7use crate::metadata::RemoteMetadata;
8
9#[derive(Clone, Debug)]
10pub struct HttpClientConfig {
11 pub user_agent: String,
12 pub connect_timeout: Duration,
13 pub timeout: Duration,
14}
15
16impl Default for HttpClientConfig {
17 fn default() -> Self {
18 Self {
19 user_agent: format!("Fetchr/{}", env!("CARGO_PKG_VERSION")),
20 connect_timeout: Duration::from_secs(10),
21 timeout: Duration::from_secs(30),
22 }
23 }
24}
25
26#[derive(Clone, Debug)]
27pub struct HttpClient {
28 inner: Client,
29}
30
31impl HttpClient {
32 pub fn new(config: HttpClientConfig) -> Result<Self> {
33 let client = Client::builder()
34 .user_agent(config.user_agent)
35 .connect_timeout(config.connect_timeout)
36 .timeout(config.timeout)
37 .build()?;
38
39 Ok(Self { inner: client })
40 }
41
42 pub async fn probe_metadata(&self, url: &str) -> Result<RemoteMetadata> {
44 let parsed_url = Url::parse(url).map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
45
46 let head_res = self.inner.head(parsed_url.clone()).send().await;
48 if let Ok(res) = head_res {
49 if res.status().is_success() {
50 let final_url = res.url().to_string();
51 let headers = res.headers();
52 return Ok(RemoteMetadata::from_headers(final_url, headers));
53 }
54 }
55
56 let get_res = self
58 .inner
59 .get(parsed_url)
60 .header(RANGE, "bytes=0-0")
61 .send()
62 .await?;
63
64 if !get_res.status().is_success() && get_res.status() != StatusCode::PARTIAL_CONTENT {
65 return Err(HttpError::HttpStatus {
66 status: get_res.status().as_u16(),
67 message: get_res.status().canonical_reason().unwrap_or("Unknown").to_string(),
68 });
69 }
70
71 let final_url = get_res.url().to_string();
72 let headers = get_res.headers();
73 let mut metadata = RemoteMetadata::from_headers(final_url, headers);
74
75 if get_res.status() == StatusCode::PARTIAL_CONTENT {
77 metadata.accept_ranges = true;
78 if let Some(content_range) = headers.get(reqwest::header::CONTENT_RANGE) {
80 if let Ok(range_str) = content_range.to_str() {
81 if let Some(total_str) = range_str.split('/').nth(1) {
83 if let Ok(total_len) = total_str.parse::<u64>() {
84 metadata.content_length = Some(total_len);
85 }
86 }
87 }
88 }
89 }
90
91 Ok(metadata)
92 }
93
94 pub async fn fetch_stream(
96 &self,
97 url: &str,
98 start_byte: u64,
99 end_byte: Option<u64>,
100 ) -> Result<(StatusCode, HeaderMap, reqwest::Response)> {
101 let parsed_url = Url::parse(url).map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
102 let mut req = self.inner.get(parsed_url);
103
104 if start_byte > 0 || end_byte.is_some() {
105 let range_val = match end_byte {
106 Some(end) => format!("bytes={}-{}", start_byte, end),
107 None => format!("bytes={}-", start_byte),
108 };
109 req = req.header(RANGE, range_val);
110 }
111
112 let res = req.send().await?;
113 let status = res.status();
114 let headers = res.headers().clone();
115
116 if !status.is_success() && status != StatusCode::PARTIAL_CONTENT {
117 return Err(HttpError::HttpStatus {
118 status: status.as_u16(),
119 message: status.canonical_reason().unwrap_or("Unknown").to_string(),
120 });
121 }
122
123 Ok((status, headers, res))
124 }
125}
126
127impl Default for HttpClient {
128 fn default() -> Self {
129 Self::new(HttpClientConfig::default()).expect("Failed to build default HttpClient")
130 }
131}