use std::fs;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use anyhow::Context;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default_host")]
pub host: String,
pub listen: Option<String>,
#[serde(default = "default_port")]
pub port: u16,
pub max_memory_mb: Option<u64>,
pub read_buffer_kb: Option<usize>,
pub username: Option<String>,
pub password: Option<String>,
#[serde(default = "default_max_failed_attempts")]
pub max_failed_attempts: u32,
#[serde(default = "default_lockout_duration")]
pub lockout_duration_seconds: u64,
}
fn default_host() -> String {
"127.0.0.1".to_string()
}
fn default_port() -> u16 {
7777
}
fn default_max_failed_attempts() -> u32 {
3
}
fn default_lockout_duration() -> u64 {
600 }
impl Config {
pub fn load() -> anyhow::Result<Self> {
let config_paths = if let Ok(env_path) = std::env::var("RUST_CACHE_CONFIG") {
vec![env_path]
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
vec![
"/etc/rust-cache/rust-cache.conf".to_string(),
format!("{}/.config/rust-cache/rust-cache.toml", home),
"rust-cache.toml".to_string(),
]
};
for path in config_paths {
if let Ok(text) = fs::read_to_string(&path) {
let mut cfg: Config = toml::from_str(&text)
.with_context(|| format!("failed to parse config file at {path}"))?;
if cfg.max_memory_mb.is_none() {
cfg.max_memory_mb = Self::auto_max_memory_mb();
}
return Ok(cfg);
}
}
Self::default_with_auto()
}
fn default_with_auto() -> anyhow::Result<Self> {
let max_memory_mb = Self::auto_max_memory_mb();
Ok(Config {
host: default_host(),
listen: None,
port: default_port(),
max_memory_mb,
read_buffer_kb: None,
username: None,
password: None,
max_failed_attempts: default_max_failed_attempts(),
lockout_duration_seconds: default_lockout_duration(),
})
}
fn auto_max_memory_mb() -> Option<u64> {
detect_total_memory_bytes().map(|bytes| {
(bytes / 2) / (1024 * 1024)
})
}
pub fn addr(&self) -> SocketAddr {
let host_str = self.listen.as_ref().unwrap_or(&self.host);
let ip: IpAddr = host_str
.parse()
.unwrap_or_else(|_| IpAddr::V4(Ipv4Addr::LOCALHOST));
SocketAddr::new(ip, self.port)
}
pub fn requires_auth(&self) -> bool {
self.username.is_some() || self.password.is_some()
}
pub fn check_auth(&self, username: Option<&str>, password: Option<&str>) -> bool {
if !self.requires_auth() {
return true;
}
let username_ok = self.username.as_ref().map(|u| u.as_str()) == username;
let password_ok = self.password.as_ref().map(|p| p.as_str()) == password;
if self.username.is_some() {
username_ok && password_ok
} else {
password_ok
}
}
pub fn max_memory_bytes(&self) -> Option<u64> {
self.max_memory_mb.map(|mb| mb * 1024 * 1024)
}
pub fn read_buffer_size(&self) -> usize {
self.read_buffer_kb.unwrap_or(4) * 1024
}
}
fn detect_total_memory_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let contents = fs::read_to_string("/proc/meminfo").ok()?;
for line in contents.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kb_str = rest
.split_whitespace()
.next()
.unwrap_or_default();
if let Ok(kb) = kb_str.parse::<u64>() {
return Some(kb * 1024);
}
}
}
None
}
#[cfg(not(target_os = "linux"))]
{
Some(1024 * 1024 * 1024)
}
}