ctoolbox/storage/
pc_settings.rs1use anyhow::{Context, Result};
11use fs2::FileExt;
12use serde::{Deserialize, Serialize};
13use std::fs::OpenOptions;
14use std::io::{Read, Seek, SeekFrom, Write};
15use std::path::PathBuf;
16
17use crate::storage::get_storage_dir;
18use crate::utilities::get_current_test_name;
19
20pub static DEFAULT_SHOW_USERS: bool = true;
21pub static DEFAULT_SERVER_URL: &str = "https://collectivetoolbox.com";
22pub static DEFAULT_BIND_TO_IP: &str = "127.0.0.1";
23pub static DEFAULT_KIOSK_MODE: bool = false;
24pub static DEFAULT_LOG_STACK_FILE: bool = false;
25pub static DEFAULT_FEATURE_LOGIN: bool = true;
26pub static DEFAULT_FEATURE_REGISTRATION: bool = true;
27
28#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
29#[allow(clippy::struct_excessive_bools)]
30#[serde(default)]
31pub struct PcSettings {
32 pub show_users: bool,
33 pub bind_to_ip: String,
34 pub server_url: String,
35 pub domain_name: Option<String>,
36 pub fixed_http_port: Option<u16>,
37 pub fixed_https_port: Option<u16>,
38 pub http_redirect: bool,
39 pub tls_certificate: Option<String>,
40 pub tls_private_key: Option<String>,
41 pub admin_users: Vec<u64>,
42 pub admin_password_hash: Option<String>,
44 pub kiosk_mode: bool,
45 pub log_stack_file: bool,
46 pub feature_login: bool,
47 pub feature_registration: bool,
48 pub version: u32,
49 pub note: String,
50}
51
52impl Default for PcSettings {
53 fn default() -> Self {
54 PcSettings {
55 show_users: DEFAULT_SHOW_USERS,
56 bind_to_ip: DEFAULT_BIND_TO_IP.into(),
57 server_url: DEFAULT_SERVER_URL.into(),
58 domain_name: None,
59 fixed_http_port: None,
60 fixed_https_port: None,
61 http_redirect: true,
62 tls_certificate: None,
63 tls_private_key: None,
64 admin_users: Vec::new(),
65 admin_password_hash: None,
66 kiosk_mode: DEFAULT_KIOSK_MODE,
67 log_stack_file: DEFAULT_LOG_STACK_FILE,
68 feature_login: DEFAULT_FEATURE_LOGIN,
69 feature_registration: DEFAULT_FEATURE_REGISTRATION,
70 version: 1,
71 note: "Unsigned default settings (stub).".into(),
72 }
73 }
74}
75
76pub fn get_str_setting(setting: &str) -> Option<String> {
77 let settings = get_settings();
78 match setting {
79 "bind_to_ip" => Some(settings.bind_to_ip),
80 "server_url" => Some(settings.server_url),
81 "domain_name" => settings.domain_name,
82 _ => None,
83 }
84}
85
86pub fn get_bool_setting(setting: &str) -> bool {
87 let settings = get_settings();
88 match setting {
89 "show_users" => settings.show_users,
90 "http_redirect" => settings.http_redirect,
91 "kiosk_mode" => settings.kiosk_mode,
92 "log_stack_file" => settings.log_stack_file,
93 _ => unimplemented!(),
94 }
95}
96
97impl PcSettings {
98 fn settings_path() -> anyhow::Result<PathBuf> {
114 if cfg!(test) {
116
117 }
118
119 let mut path = get_storage_dir()?.join("config");
120 std::fs::create_dir_all(&path)?;
121 let filename = if cfg!(test) {
122 format!("{}_pc_settings.json", get_current_test_name())
123 } else {
124 "pc_settings.json".to_string()
125 };
126 path.push(filename);
127 Ok(path)
128 }
129
130 pub fn load() -> Result<Self> {
134 let path = Self::settings_path()?;
135 if !std::fs::exists(path.as_path())? {
136 PcSettings::default().save()?;
137 }
138 let file =
139 OpenOptions::new().read(true).open(&path).with_context(|| {
140 format!("Failed to open settings file: {}", path.display())
141 })?;
142
143 file.lock_shared()
145 .context("Failed to acquire shared lock on settings file")?;
146
147 let mut contents = String::new();
148 std::io::Read::read_to_string(&mut &file, &mut contents)
149 .context("Failed to read settings file")?;
150
151 file.unlock()?;
153
154 if contents.trim().is_empty() {
155 anyhow::bail!("Settings file is empty");
156 }
157
158 let settings = serde_json::from_str(&contents)
159 .context("Failed to parse settings JSON")?;
160 Ok(settings)
161 }
162
163 pub fn save(&self) -> Result<()> {
165 let path = Self::settings_path()?;
166 let mut file = OpenOptions::new()
167 .write(true)
168 .create(true)
169 .truncate(true)
170 .open(&path)
171 .with_context(|| {
172 format!(
173 "Failed to open settings file for writing: {}",
174 path.display()
175 )
176 })?;
177
178 file.lock_exclusive()
180 .context("Failed to acquire exclusive lock on settings file")?;
181
182 let data = serde_json::to_string_pretty(self)
183 .context("Failed to serialize settings")?;
184 file.seek(SeekFrom::Start(0))?;
185 file.write_all(data.as_bytes())
186 .context("Failed to write settings file")?;
187 file.set_len(
188 u64::try_from(data.len()).context("Failed to set file length")?,
189 )?;
190
191 file.unlock()?;
193
194 Ok(())
195 }
196}
197
198pub fn ensure_pc_settings() -> Result<()> {
199 PcSettings::load()?;
200 Ok(())
201}
202
203pub fn get_settings() -> PcSettings {
204 crate::storage::pc_settings::PcSettings::load().unwrap_or_default()
205}
206
207#[cfg(test)]
208#[allow(clippy::unwrap_in_result, clippy::panic_in_result_fn)]
209mod tests {
210 use super::PcSettings;
211 use anyhow::Result;
212
213 #[crate::ctb_test]
214 fn test_save_and_load_settings() -> Result<()> {
215 let old_settings = PcSettings::load()?;
216 let settings = PcSettings {
217 bind_to_ip: "0.0.0.0".into(),
218 fixed_http_port: Some(8080),
219 fixed_https_port: Some(8443),
220 admin_users: vec![1, 2, 3],
221 ..Default::default()
222 };
223 settings.save()?;
224
225 let loaded_settings = PcSettings::load()?;
226 assert_eq!(settings, loaded_settings);
227 old_settings.save()?;
228 assert_eq!(old_settings, PcSettings::load()?);
229 Ok(())
230 }
231}