152 lines
4.9 KiB
Rust
152 lines
4.9 KiB
Rust
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,
|
|
})
|
|
}
|