use std::time::Duration;
use reqwest::header::{HeaderMap, RANGE};
use reqwest::{Client, StatusCode};
use url::Url;
use crate::error::{HttpError, Result};
use crate::metadata::RemoteMetadata;
#[derive(Clone, Debug)]
pub struct HttpClientConfig {
pub user_agent: String,
pub connect_timeout: Duration,
pub timeout: Duration,
}
impl Default for HttpClientConfig {
fn default() -> Self {
Self {
user_agent: format!("Fetchr/{}", env!("CARGO_PKG_VERSION")),
connect_timeout: Duration::from_secs(10),
timeout: Duration::from_secs(30),
}
}
}
#[derive(Clone, Debug)]
pub struct HttpClient {
inner: Client,
}
impl HttpClient {
pub fn new(config: HttpClientConfig) -> Result<Self> {
let client = Client::builder()
.user_agent(config.user_agent)
.connect_timeout(config.connect_timeout)
.timeout(config.timeout)
.build()?;
Ok(Self { inner: client })
}
pub async fn probe_metadata(&self, url: &str) -> Result<RemoteMetadata> {
let parsed_url = Url::parse(url).map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
let head_res = self.inner.head(parsed_url.clone()).send().await;
if let Ok(res) = head_res {
if res.status().is_success() {
let final_url = res.url().to_string();
let headers = res.headers();
return Ok(RemoteMetadata::from_headers(final_url, headers));
}
}
let get_res = self
.inner
.get(parsed_url)
.header(RANGE, "bytes=0-0")
.send()
.await?;
if !get_res.status().is_success() && get_res.status() != StatusCode::PARTIAL_CONTENT {
return Err(HttpError::HttpStatus {
status: get_res.status().as_u16(),
message: get_res.status().canonical_reason().unwrap_or("Unknown").to_string(),
});
}
let final_url = get_res.url().to_string();
let headers = get_res.headers();
let mut metadata = RemoteMetadata::from_headers(final_url, headers);
if get_res.status() == StatusCode::PARTIAL_CONTENT {
metadata.accept_ranges = true;
if let Some(content_range) = headers.get(reqwest::header::CONTENT_RANGE) {
if let Ok(range_str) = content_range.to_str() {
if let Some(total_str) = range_str.split('/').nth(1) {
if let Ok(total_len) = total_str.parse::<u64>() {
metadata.content_length = Some(total_len);
}
}
}
}
}
Ok(metadata)
}
pub async fn fetch_stream(
&self,
url: &str,
start_byte: u64,
end_byte: Option<u64>,
) -> Result<(StatusCode, HeaderMap, reqwest::Response)> {
let parsed_url = Url::parse(url).map_err(|_| HttpError::InvalidUrl(url.to_string()))?;
let mut req = self.inner.get(parsed_url);
if start_byte > 0 || end_byte.is_some() {
let range_val = match end_byte {
Some(end) => format!("bytes={}-{}", start_byte, end),
None => format!("bytes={}-", start_byte),
};
req = req.header(RANGE, range_val);
}
let res = req.send().await?;
let status = res.status();
let headers = res.headers().clone();
if !status.is_success() && status != StatusCode::PARTIAL_CONTENT {
return Err(HttpError::HttpStatus {
status: status.as_u16(),
message: status.canonical_reason().unwrap_or("Unknown").to_string(),
});
}
Ok((status, headers, res))
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new(HttpClientConfig::default()).expect("Failed to build default HttpClient")
}
}