Part O
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
const MAX_TTL_MS: i64 = 48 * 60 * 60 * 1000; // 48 hours hard maximum
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub storage_dir: PathBuf,
|
||||
pub static_dir: PathBuf,
|
||||
pub max_file_size: u64,
|
||||
pub file_ttl_ms: i64,
|
||||
pub max_storage_usage: f64,
|
||||
pub key_length: usize,
|
||||
pub public_base_url: Option<String>,
|
||||
pub rate_limit_enabled: bool,
|
||||
pub rate_limit_max: usize,
|
||||
pub rate_limit_window_ms: u64,
|
||||
pub trust_proxy: bool,
|
||||
}
|
||||
|
||||
pub fn parse_size(value: &str) -> Result<u64, String> {
|
||||
let s = value.trim();
|
||||
let split = s.find(|c: char| !c.is_ascii_digit() && c != '.');
|
||||
let (num_part, unit_part) = match split {
|
||||
Some(i) => (&s[..i], &s[i..]),
|
||||
None => (s, ""),
|
||||
};
|
||||
let num: f64 = num_part
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| format!("Invalid size value: \"{value}\""))?;
|
||||
let unit = unit_part.trim().to_uppercase();
|
||||
let factor: f64 = match unit.as_str() {
|
||||
"B" | "" => 1.0,
|
||||
"K" | "KB" | "KIB" => 1024.0,
|
||||
"M" | "MB" | "MIB" => 1024.0f64.powi(2),
|
||||
"G" | "GB" | "GIB" => 1024.0f64.powi(3),
|
||||
"T" | "TB" | "TIB" => 1024.0f64.powi(4),
|
||||
_ => return Err(format!("Invalid size unit in: \"{value}\"")),
|
||||
};
|
||||
Ok((num * factor) as u64)
|
||||
}
|
||||
|
||||
fn env(name: &str) -> Option<String> {
|
||||
std::env::var(name).ok().filter(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
fn bool_from_env(value: Option<&str>, fallback: bool) -> bool {
|
||||
match value {
|
||||
Some(v) => !["0", "false", "no", "off"].contains(&v.trim().to_lowercase().as_str()),
|
||||
None => fallback,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load() -> Result<Config, String> {
|
||||
let port = env("PORT")
|
||||
.unwrap_or_else(|| "3000".to_string())
|
||||
.parse::<u16>()
|
||||
.map_err(|_| format!("Invalid PORT: {:?}", env("PORT")))?;
|
||||
if port == 0 {
|
||||
return Err("Invalid PORT".to_string());
|
||||
}
|
||||
|
||||
let host = env("HOST").unwrap_or_else(|| "0.0.0.0".to_string());
|
||||
|
||||
let storage_dir = PathBuf::from(env("STORAGE_DIR").unwrap_or_else(|| "./storage".to_string()));
|
||||
let static_dir = PathBuf::from(env("STATIC_DIR").unwrap_or_else(|| "./frontend/dist".to_string()));
|
||||
|
||||
let max_file_size = parse_size(
|
||||
&env("MAX_FILE_SIZE").unwrap_or_else(|| "10GB".to_string()),
|
||||
)?;
|
||||
if max_file_size < 1024 {
|
||||
return Err("MAX_FILE_SIZE must be at least 1KB".to_string());
|
||||
}
|
||||
|
||||
let file_ttl_ms_raw = env("FILE_TTL_MS")
|
||||
.unwrap_or_else(|| MAX_TTL_MS.to_string())
|
||||
.parse::<i64>()
|
||||
.map_err(|_| "Invalid FILE_TTL_MS".to_string())?;
|
||||
if file_ttl_ms_raw < 1 {
|
||||
return Err("Invalid FILE_TTL_MS".to_string());
|
||||
}
|
||||
let file_ttl_ms = file_ttl_ms_raw.min(MAX_TTL_MS);
|
||||
|
||||
let max_storage_usage = env("MAX_STORAGE_USAGE")
|
||||
.unwrap_or_else(|| "0.5".to_string())
|
||||
.parse::<f64>()
|
||||
.map_err(|_| "Invalid MAX_STORAGE_USAGE".to_string())?;
|
||||
if !(0.0..=1.0).contains(&max_storage_usage) {
|
||||
return Err("Invalid MAX_STORAGE_USAGE".to_string());
|
||||
}
|
||||
|
||||
let key_length = env("KEY_LENGTH")
|
||||
.unwrap_or_else(|| "8".to_string())
|
||||
.parse::<usize>()
|
||||
.map_err(|_| "Invalid KEY_LENGTH".to_string())?;
|
||||
if !(4..=64).contains(&key_length) {
|
||||
return Err("Invalid KEY_LENGTH".to_string());
|
||||
}
|
||||
|
||||
let rate_limit_max = env("RATE_LIMIT_MAX")
|
||||
.unwrap_or_else(|| "300".to_string())
|
||||
.parse::<usize>()
|
||||
.map_err(|_| "Invalid RATE_LIMIT_MAX".to_string())?;
|
||||
if rate_limit_max < 1 {
|
||||
return Err("Invalid RATE_LIMIT_MAX".to_string());
|
||||
}
|
||||
|
||||
let rate_limit_window_ms = env("RATE_LIMIT_WINDOW_MS")
|
||||
.unwrap_or_else(|| "60000".to_string())
|
||||
.parse::<u64>()
|
||||
.map_err(|_| "Invalid RATE_LIMIT_WINDOW_MS".to_string())?;
|
||||
if rate_limit_window_ms < 1000 {
|
||||
return Err("Invalid RATE_LIMIT_WINDOW_MS".to_string());
|
||||
}
|
||||
|
||||
let trust_proxy = bool_from_env(env("TRUST_PROXY").as_deref(), false);
|
||||
let rate_limit_enabled = bool_from_env(env("RATE_LIMIT_ENABLED").as_deref(), true);
|
||||
|
||||
let public_base_url = match env("PUBLIC_BASE_URL") {
|
||||
Some(raw) => {
|
||||
let normalized = raw.trim_end_matches('/').to_string();
|
||||
let valid = (normalized.starts_with("http://") || normalized.starts_with("https://"))
|
||||
&& !normalized[normalized.find("://").map(|i| i + 3).unwrap_or(0)..]
|
||||
.contains('/')
|
||||
&& !normalized.chars().any(char::is_whitespace);
|
||||
if !valid {
|
||||
return Err(format!("Invalid PUBLIC_BASE_URL: \"{raw}\""));
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Config {
|
||||
host,
|
||||
port,
|
||||
storage_dir,
|
||||
static_dir,
|
||||
max_file_size,
|
||||
file_ttl_ms,
|
||||
max_storage_usage,
|
||||
key_length,
|
||||
public_base_url,
|
||||
rate_limit_enabled,
|
||||
rate_limit_max,
|
||||
rate_limit_window_ms,
|
||||
trust_proxy,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user