use std::fmt::Write as _;
use super::StorageError;
pub(super) const MAX_PRESIGN_SECS: u64 = 604_800;
pub(super) const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
pub(super) const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
const MIN_UPLOAD_RATE: u64 = 256 * 1024;
pub(super) enum Http {
Default {
http: reqwest::Client,
upload: reqwest::Client,
read_timeout: std::time::Duration,
},
Custom(reqwest::Client),
}
impl Http {
pub(super) fn defaults() -> Self {
Self::with_read_timeout(DEFAULT_READ_TIMEOUT)
}
pub(super) fn with_read_timeout(read_timeout: std::time::Duration) -> Self {
let base = || {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(DEFAULT_CONNECT_TIMEOUT)
};
Self::Default {
http: base()
.read_timeout(read_timeout)
.build()
.expect("reqwest client builds"),
upload: base().build().expect("reqwest client builds"),
read_timeout,
}
}
pub(super) fn request(
&self,
method: reqwest::Method,
url: &str,
len: usize,
) -> reqwest::RequestBuilder {
match self {
Self::Default {
upload,
read_timeout,
..
} if method == reqwest::Method::PUT => upload
.request(method, url)
.timeout(upload_timeout(*read_timeout, len)),
Self::Default { http, .. } | Self::Custom(http) => http.request(method, url),
}
}
}
pub(super) fn upload_timeout(read_timeout: std::time::Duration, len: usize) -> std::time::Duration {
let len = u64::try_from(len).unwrap_or(u64::MAX);
read_timeout + std::time::Duration::from_millis(len.saturating_mul(1000) / MIN_UPLOAD_RATE)
}
pub(super) fn canonical_header_value(v: Option<&str>) -> Result<Option<String>, StorageError> {
let Some(v) = v else { return Ok(None) };
if v.chars().any(|c| c.is_control() && c != '\t') {
return Err(StorageError::Io(format!(
"content type {v:?} has a control character"
)));
}
let v = v
.split([' ', '\t'])
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
Ok((!v.is_empty()).then_some(v))
}
pub(super) fn encode_key(key: &str) -> String {
percent_encode(key, true)
}
pub(super) fn encode_query(s: &str) -> String {
percent_encode(s, false)
}
fn percent_encode(s: &str, keep_slash: bool) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
if b.is_ascii_alphanumeric()
|| matches!(b, b'-' | b'_' | b'.' | b'~')
|| (keep_slash && b == b'/')
{
out.push(b as char);
} else {
let _ = write!(out, "%{b:02X}");
}
}
out
}
#[cfg(feature = "storage-azure")]
pub(super) fn split_endpoint(endpoint: &str) -> (String, String) {
let (scheme, rest) = if let Some(r) = endpoint.strip_prefix("http://") {
("http", r)
} else {
(
"https",
endpoint.strip_prefix("https://").unwrap_or(endpoint),
)
};
let rest = rest.trim_end_matches('/');
match rest.find('/') {
Some(i) => (format!("{scheme}://{}", &rest[..i]), rest[i..].to_owned()),
None => (format!("{scheme}://{rest}"), String::new()),
}
}
#[cfg(all(test, feature = "storage-azure"))]
mod tests {
use super::*;
#[test]
fn split_endpoint_separates_origin_and_path() {
assert_eq!(
split_endpoint("http://127.0.0.1:10000/devstoreaccount1/"),
("http://127.0.0.1:10000".into(), "/devstoreaccount1".into())
);
assert_eq!(
split_endpoint("https://acct.blob.core.windows.net"),
("https://acct.blob.core.windows.net".into(), String::new())
);
assert_eq!(
split_endpoint("cdn.example.com/files"),
("https://cdn.example.com".into(), "/files".into())
);
}
}