use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use dragoon_proto::constants;
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct ServerSection {
data_dir: PathBuf,
#[serde(default = "default_bind_host")]
bind_host: String,
#[serde(default = "default_bind_port")]
bind_port: u16,
#[serde(default = "default_public_url")]
public_url: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct StorageSection {
soft_quota_bytes: Option<u64>,
default_ttl_days: Option<u64>,
hard_min_free_pct: Option<u32>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct SecuritySection {
session_ttl_hours: Option<i64>,
totp_issuer: Option<String>,
worker_register_code_ttl_sec: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigFile {
server: ServerSection,
#[serde(default)]
storage: StorageSection,
#[serde(default)]
security: SecuritySection,
}
fn default_bind_host() -> String {
"127.0.0.1".into()
}
fn default_bind_port() -> u16 {
8000
}
fn default_public_url() -> String {
"http://127.0.0.1:8000".into()
}
#[derive(Debug, Clone)]
pub struct Settings {
pub data_dir: PathBuf,
pub public_url: String,
pub bind_host: String,
pub bind_port: u16,
pub session_ttl_hours: i64,
pub totp_issuer: String,
pub worker_register_code_ttl_sec: i64,
pub soft_quota_bytes: u64,
pub default_ttl_days: u64,
pub hard_min_free_pct: u32,
pub log_long_poll_sec: f64,
pub log_long_poll_step_sec: f64,
}
impl Settings {
pub fn from_toml_path(path: impl AsRef<Path>) -> Result<Self> {
let raw = std::fs::read_to_string(path.as_ref())
.with_context(|| format!("read {}", path.as_ref().display()))?;
let cfg: ConfigFile = toml::from_str(&raw).with_context(|| "parse server toml")?;
Ok(Settings::from(cfg))
}
pub fn for_test(data_dir: impl Into<PathBuf>) -> Self {
Self {
data_dir: data_dir.into(),
public_url: default_public_url(),
bind_host: default_bind_host(),
bind_port: 0,
session_ttl_hours: constants::SESSION_TTL_HOURS_DEFAULT,
totp_issuer: "RemoteExecutor".into(),
worker_register_code_ttl_sec: constants::WORKER_REGISTER_CODE_TTL_SEC,
soft_quota_bytes: constants::DEFAULT_SOFT_QUOTA_BYTES,
default_ttl_days: constants::DEFAULT_TTL_DAYS,
hard_min_free_pct: constants::DEFAULT_HARD_MIN_FREE_PCT,
log_long_poll_sec: 0.5,
log_long_poll_step_sec: 0.05,
}
}
pub fn db_path(&self) -> PathBuf {
self.data_dir.join("re.sqlite")
}
pub fn blobs_dir(&self) -> PathBuf {
self.data_dir.join("blobs")
}
}
impl From<ConfigFile> for Settings {
fn from(cfg: ConfigFile) -> Self {
Self {
data_dir: cfg.server.data_dir,
public_url: cfg.server.public_url,
bind_host: cfg.server.bind_host,
bind_port: cfg.server.bind_port,
session_ttl_hours: cfg
.security
.session_ttl_hours
.unwrap_or(constants::SESSION_TTL_HOURS_DEFAULT),
totp_issuer: cfg
.security
.totp_issuer
.unwrap_or_else(|| "RemoteExecutor".into()),
worker_register_code_ttl_sec: cfg
.security
.worker_register_code_ttl_sec
.unwrap_or(constants::WORKER_REGISTER_CODE_TTL_SEC),
soft_quota_bytes: cfg
.storage
.soft_quota_bytes
.unwrap_or(constants::DEFAULT_SOFT_QUOTA_BYTES),
default_ttl_days: cfg
.storage
.default_ttl_days
.unwrap_or(constants::DEFAULT_TTL_DAYS),
hard_min_free_pct: cfg
.storage
.hard_min_free_pct
.unwrap_or(constants::DEFAULT_HARD_MIN_FREE_PCT),
log_long_poll_sec: 25.0,
log_long_poll_step_sec: 0.5,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_minimal_toml() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = dir.path().join("server.toml");
std::fs::write(
&cfg_path,
"[server]\ndata_dir = \"/tmp/re\"\nbind_port = 9000\n",
)
.unwrap();
let s = Settings::from_toml_path(&cfg_path).unwrap();
assert_eq!(s.data_dir, PathBuf::from("/tmp/re"));
assert_eq!(s.bind_port, 9000);
assert_eq!(s.bind_host, "127.0.0.1");
assert_eq!(s.session_ttl_hours, constants::SESSION_TTL_HOURS_DEFAULT);
}
#[test]
fn rejects_unknown_keys() {
let dir = tempfile::tempdir().unwrap();
let cfg_path = dir.path().join("server.toml");
std::fs::write(
&cfg_path,
"[server]\ndata_dir = \"/tmp/re\"\nbogus = true\n",
)
.unwrap();
assert!(Settings::from_toml_path(&cfg_path).is_err());
}
}