use std::io::Write;
use std::path::Path;
use reqwest::blocking::Client;
use reqwest::header::{
HeaderMap, HeaderName, HeaderValue, ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED,
};
use reqwest::StatusCode;
use super::{env_secs, Conditional, FetchResult, FetchStatus, Transport};
use crate::auth::AuthResolver;
use crate::error::{Error, Result};
use crate::key::Sha256Writer;
pub struct HttpTransport {
client: Client,
}
impl HttpTransport {
pub fn new() -> Self {
let client = Client::builder()
.user_agent(concat!("earthsciio/", env!("CARGO_PKG_VERSION")))
.connect_timeout(env_secs("EARTHSCIIO_HTTP_CONNECT_TIMEOUT_SECS", 30))
.timeout(env_secs("EARTHSCIIO_HTTP_READ_TIMEOUT_SECS", 600))
.build()
.expect("default reqwest blocking client builds");
Self { client }
}
pub fn with_client(client: Client) -> Self {
Self { client }
}
}
impl Default for HttpTransport {
fn default() -> Self {
Self::new()
}
}
impl Transport for HttpTransport {
fn schemes(&self) -> &'static [&'static str] {
&["http", "https"]
}
fn fetch(
&self,
url: &str,
dest: &Path,
conditional: &Conditional,
auth: Option<&dyn AuthResolver>,
) -> Result<FetchResult> {
self.fetch_hashed(url, dest, conditional, auth)
.map(|(result, _)| result)
}
fn fetch_hashed(
&self,
url: &str,
dest: &Path,
conditional: &Conditional,
auth: Option<&dyn AuthResolver>,
) -> Result<(FetchResult, Option<String>)> {
let mut headers = HeaderMap::new();
if let Some(etag) = &conditional.etag {
if let Ok(v) = HeaderValue::from_str(etag) {
headers.insert(IF_NONE_MATCH, v);
}
}
if let Some(lm) = &conditional.last_modified {
if let Ok(v) = HeaderValue::from_str(lm) {
headers.insert(IF_MODIFIED_SINCE, v);
}
}
if let Some(resolver) = auth {
for (name, value) in resolver.headers() {
match (
HeaderName::from_bytes(name.as_bytes()),
HeaderValue::from_str(&value),
) {
(Ok(n), Ok(v)) => {
headers.insert(n, v);
}
_ => {
return Err(Error::Transport {
url: url.to_string(),
detail: format!("invalid auth header '{name}'"),
})
}
}
}
}
let mut resp =
self.client
.get(url)
.headers(headers)
.send()
.map_err(|e| Error::Transport {
url: url.to_string(),
detail: e.to_string(),
})?;
let status = resp.status();
if status == StatusCode::NOT_MODIFIED {
return Ok((
FetchResult {
status: FetchStatus::NotModified,
etag: conditional.etag.clone(),
last_modified: conditional.last_modified.clone(),
bytes_written: 0,
},
None,
));
}
if !status.is_success() {
if status == StatusCode::NOT_FOUND || status == StatusCode::GONE {
return Err(Error::NotFound {
url: url.to_string(),
detail: format!("HTTP {}", status.as_u16()),
});
}
return Err(Error::Transport {
url: url.to_string(),
detail: format!("HTTP {}", status.as_u16()),
});
}
let etag = header_string(resp.headers(), &ETAG);
let last_modified = header_string(resp.headers(), &LAST_MODIFIED);
let file =
std::fs::File::create(dest).map_err(|e| Error::io(Some(dest.to_path_buf()), e))?;
let mut writer = Sha256Writer::new(file);
let bytes_written = resp.copy_to(&mut writer).map_err(|e| Error::Transport {
url: url.to_string(),
detail: e.to_string(),
})?;
writer
.flush()
.map_err(|e| Error::io(Some(dest.to_path_buf()), e))?;
let sha = writer.finalize();
Ok((
FetchResult {
status: FetchStatus::Downloaded,
etag,
last_modified,
bytes_written,
},
Some(sha),
))
}
}
fn header_string(headers: &HeaderMap, name: &HeaderName) -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(String::from)
}