use std::time::Duration;
pub const ROUTE_KEY: &str = "http.upload";
pub const RATE_KEY: &str = "http.upload_min_rate";
pub const GRACE_KEY: &str = "http.upload_grace";
pub const CEILING_KEY: &str = "http.spill_ceiling";
pub const TOTAL_KEY: &str = "http.spill_total";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadConfig {
pub routes: Vec<(String, u64)>,
pub min_rate: u64,
pub grace: Duration,
pub ceiling: u64,
pub total: u64,
}
impl Default for UploadConfig {
fn default() -> Self {
UploadConfig {
routes: Vec::new(),
min_rate: ikigai_web::DEFAULT_UPLOAD_MIN_RATE,
grace: ikigai_web::DEFAULT_UPLOAD_GRACE,
ceiling: ikigai_core::DEFAULT_STREAM_SPILL,
total: 4 * ikigai_core::DEFAULT_STREAM_SPILL,
}
}
}
pub fn uploads_from(entries: &[(String, String)]) -> Result<UploadConfig, String> {
let mut config = UploadConfig::default();
let mut seen: Vec<&str> = Vec::new();
for (key, value) in entries {
let key = key.as_str();
if key != ROUTE_KEY {
if seen.contains(&key) {
return Err(format!("`{key}` is set twice; set it once"));
}
seen.push(key);
}
match key {
ROUTE_KEY => {
let shape = || {
format!(
"`{ROUTE_KEY} = \"{value}\"` is not `<path pattern> <largest body in \
bytes>`, e.g. `/media/{{name}} 4294967296`"
)
};
let mut words = value.split_whitespace();
let (Some(pattern), Some(max), None) = (words.next(), words.next(), words.next())
else {
return Err(shape());
};
if !pattern.starts_with('/') {
return Err(shape());
}
if config.routes.iter().any(|(p, _)| p == pattern) {
return Err(format!(
"`{ROUTE_KEY}` names `{pattern}` twice; give each pattern one line"
));
}
config
.routes
.push((pattern.to_string(), whole_number(ROUTE_KEY, max)?));
}
RATE_KEY => config.min_rate = whole_number(RATE_KEY, value)?,
GRACE_KEY => config.grace = Duration::from_secs(whole_number(GRACE_KEY, value)?),
CEILING_KEY => config.ceiling = whole_number(CEILING_KEY, value)?,
TOTAL_KEY => config.total = whole_number(TOTAL_KEY, value)?,
other => {
return Err(format!(
"`{other}` is not an HTTP upload setting; the keys are `{ROUTE_KEY}`, \
`{RATE_KEY}`, `{GRACE_KEY}`, `{CEILING_KEY}` and `{TOTAL_KEY}`"
))
}
}
}
Ok(config)
}
fn whole_number(key: &str, value: &str) -> Result<u64, String> {
let digits: String = value.chars().filter(|c| *c != '_').collect();
if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("`{key}`: `{value}` is not a whole number"));
}
match digits.parse::<u64>() {
Ok(0) => Err(format!("`{key}`: `{value}`: the least it can be is 1")),
Ok(n) => Ok(n),
Err(_) => Err(format!("`{key}`: `{value}` is too large")),
}
}
pub fn describe(config: &UploadConfig, dir: Option<&std::path::Path>) -> String {
match (dir, config.routes.len()) {
(None, _) => "no spill".to_string(),
(Some(dir), 0) => format!("spill at {}, no upload routes", dir.display()),
(Some(dir), n) => format!(
"{n} upload route(s), spill at {} (at most {} bytes a request)",
dir.display(),
config.ceiling
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entries(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn with_nothing_configured_there_are_no_routes_and_the_defaults_hold() {
let config = uploads_from(&[]).unwrap();
assert_eq!(config, UploadConfig::default());
assert_eq!(config.ceiling, 1 << 30);
assert_eq!(config.min_rate, 16 * 1024);
}
#[test]
fn every_key_is_read() {
let config = uploads_from(&entries(&[
("http.upload", "/media/{name} 4_294_967_296"),
("http.upload", "/drop 1024"),
("http.upload_min_rate", "65536"),
("http.upload_grace", "10"),
("http.spill_ceiling", "8589934592"),
("http.spill_total", "17179869184"),
]))
.unwrap();
assert_eq!(
config.routes,
vec![
("/media/{name}".to_string(), 4_294_967_296),
("/drop".to_string(), 1024)
]
);
assert_eq!(config.min_rate, 65536);
assert_eq!(config.grace, Duration::from_secs(10));
assert_eq!(config.ceiling, 8_589_934_592);
assert_eq!(config.total, 17_179_869_184);
}
#[test]
fn a_line_that_does_not_parse_is_refused_naming_it() {
for (key, value, says) in [
("http.upload", "/media", "is not `<path pattern>"),
("http.upload", "media 10", "is not `<path pattern>"),
("http.upload", "/m 10 extra", "is not `<path pattern>"),
("http.upload", "/m ten", "is not a whole number"),
("http.upload", "/m 0", "the least it can be is 1"),
("http.upload_min_rate", "-1", "is not a whole number"),
("http.uploads", "/m 10", "is not an HTTP upload setting"),
] {
let error = uploads_from(&entries(&[(key, value)])).unwrap_err();
assert!(error.contains(says), "{key} = {value}: {error}");
}
let twice = uploads_from(&entries(&[
("http.spill_ceiling", "1"),
("http.spill_ceiling", "2"),
]))
.unwrap_err();
assert!(twice.contains("set twice"), "{twice}");
let same = uploads_from(&entries(&[
("http.upload", "/m 1"),
("http.upload", "/m 2"),
]))
.unwrap_err();
assert!(same.contains("twice"), "{same}");
}
}