dragoon_server/
settings.rs1use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result};
10use serde::Deserialize;
11
12use dragoon_proto::constants;
13
14#[derive(Debug, Clone, Deserialize)]
15#[serde(deny_unknown_fields)]
16struct ServerSection {
17 data_dir: PathBuf,
18 #[serde(default = "default_bind_host")]
19 bind_host: String,
20 #[serde(default = "default_bind_port")]
21 bind_port: u16,
22 #[serde(default = "default_public_url")]
23 public_url: String,
24}
25
26#[derive(Debug, Clone, Default, Deserialize)]
27#[serde(deny_unknown_fields)]
28struct StorageSection {
29 soft_quota_bytes: Option<u64>,
30 default_ttl_days: Option<u64>,
31 hard_min_free_pct: Option<u32>,
32}
33
34#[derive(Debug, Clone, Default, Deserialize)]
35#[serde(deny_unknown_fields)]
36struct SecuritySection {
37 session_ttl_hours: Option<i64>,
38 totp_issuer: Option<String>,
39 worker_register_code_ttl_sec: Option<i64>,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43#[serde(deny_unknown_fields)]
44struct ConfigFile {
45 server: ServerSection,
46 #[serde(default)]
47 storage: StorageSection,
48 #[serde(default)]
49 security: SecuritySection,
50}
51
52fn default_bind_host() -> String {
53 "127.0.0.1".into()
54}
55fn default_bind_port() -> u16 {
56 8000
57}
58fn default_public_url() -> String {
59 "http://127.0.0.1:8000".into()
60}
61
62#[derive(Debug, Clone)]
64pub struct Settings {
65 pub data_dir: PathBuf,
66 pub public_url: String,
67 pub bind_host: String,
68 pub bind_port: u16,
69 pub session_ttl_hours: i64,
70 pub totp_issuer: String,
71 pub worker_register_code_ttl_sec: i64,
72 pub soft_quota_bytes: u64,
73 pub default_ttl_days: u64,
74 pub hard_min_free_pct: u32,
75 pub log_long_poll_sec: f64,
76 pub log_long_poll_step_sec: f64,
77}
78
79impl Settings {
80 pub fn from_toml_path(path: impl AsRef<Path>) -> Result<Self> {
81 let raw = std::fs::read_to_string(path.as_ref())
82 .with_context(|| format!("read {}", path.as_ref().display()))?;
83 let cfg: ConfigFile = toml::from_str(&raw).with_context(|| "parse server toml")?;
84 Ok(Settings::from(cfg))
85 }
86
87 pub fn for_test(data_dir: impl Into<PathBuf>) -> Self {
88 Self {
89 data_dir: data_dir.into(),
90 public_url: default_public_url(),
91 bind_host: default_bind_host(),
92 bind_port: 0,
93 session_ttl_hours: constants::SESSION_TTL_HOURS_DEFAULT,
94 totp_issuer: "RemoteExecutor".into(),
95 worker_register_code_ttl_sec: constants::WORKER_REGISTER_CODE_TTL_SEC,
96 soft_quota_bytes: constants::DEFAULT_SOFT_QUOTA_BYTES,
97 default_ttl_days: constants::DEFAULT_TTL_DAYS,
98 hard_min_free_pct: constants::DEFAULT_HARD_MIN_FREE_PCT,
99 log_long_poll_sec: 0.5,
100 log_long_poll_step_sec: 0.05,
101 }
102 }
103
104 pub fn db_path(&self) -> PathBuf {
105 self.data_dir.join("re.sqlite")
106 }
107
108 pub fn blobs_dir(&self) -> PathBuf {
109 self.data_dir.join("blobs")
110 }
111}
112
113impl From<ConfigFile> for Settings {
114 fn from(cfg: ConfigFile) -> Self {
115 Self {
116 data_dir: cfg.server.data_dir,
117 public_url: cfg.server.public_url,
118 bind_host: cfg.server.bind_host,
119 bind_port: cfg.server.bind_port,
120 session_ttl_hours: cfg
121 .security
122 .session_ttl_hours
123 .unwrap_or(constants::SESSION_TTL_HOURS_DEFAULT),
124 totp_issuer: cfg
125 .security
126 .totp_issuer
127 .unwrap_or_else(|| "RemoteExecutor".into()),
128 worker_register_code_ttl_sec: cfg
129 .security
130 .worker_register_code_ttl_sec
131 .unwrap_or(constants::WORKER_REGISTER_CODE_TTL_SEC),
132 soft_quota_bytes: cfg
133 .storage
134 .soft_quota_bytes
135 .unwrap_or(constants::DEFAULT_SOFT_QUOTA_BYTES),
136 default_ttl_days: cfg
137 .storage
138 .default_ttl_days
139 .unwrap_or(constants::DEFAULT_TTL_DAYS),
140 hard_min_free_pct: cfg
141 .storage
142 .hard_min_free_pct
143 .unwrap_or(constants::DEFAULT_HARD_MIN_FREE_PCT),
144 log_long_poll_sec: 25.0,
145 log_long_poll_step_sec: 0.5,
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn loads_minimal_toml() {
156 let dir = tempfile::tempdir().unwrap();
157 let cfg_path = dir.path().join("server.toml");
158 std::fs::write(
159 &cfg_path,
160 "[server]\ndata_dir = \"/tmp/re\"\nbind_port = 9000\n",
161 )
162 .unwrap();
163 let s = Settings::from_toml_path(&cfg_path).unwrap();
164 assert_eq!(s.data_dir, PathBuf::from("/tmp/re"));
165 assert_eq!(s.bind_port, 9000);
166 assert_eq!(s.bind_host, "127.0.0.1");
168 assert_eq!(s.session_ttl_hours, constants::SESSION_TTL_HOURS_DEFAULT);
169 }
170
171 #[test]
172 fn rejects_unknown_keys() {
173 let dir = tempfile::tempdir().unwrap();
174 let cfg_path = dir.path().join("server.toml");
175 std::fs::write(
176 &cfg_path,
177 "[server]\ndata_dir = \"/tmp/re\"\nbogus = true\n",
178 )
179 .unwrap();
180 assert!(Settings::from_toml_path(&cfg_path).is_err());
181 }
182}