1use anyhow::{Context, Result, ensure};
4use log::info;
5use log::warn;
6use serde::{Deserialize, Serialize};
7use std::fs::OpenOptions;
8use std::fs::remove_file;
9use std::io::{Read, Write};
10use std::os::unix::prelude::*;
11use std::path::Path;
12use std::sync::RwLock;
13
14use crate::sysmod::camera::CameraConfig;
15use crate::sysmod::discord::DiscordConfig;
16use crate::sysmod::health::HealthConfig;
17use crate::sysmod::http::HttpConfig;
18use crate::sysmod::line::LineConfig;
19use crate::sysmod::openai::OpenAiConfig;
20use crate::sysmod::twitter::TwitterConfig;
21
22const CONFIG_FILE: &str = "config.toml";
24const CONFIG_DEF_FILE: &str = "config_default.toml";
26const CONFIG_CUR_FILE: &str = "config_current.toml";
28
29static CONFIG: RwLock<Option<Config>> = RwLock::new(None);
31
32#[derive(Debug, Default, Clone, Serialize, Deserialize)]
34pub struct Config {
35 #[serde(default)]
36 pub health: HealthConfig,
37 #[serde(default)]
38 pub camera: CameraConfig,
39 #[serde(default)]
40 pub twitter: TwitterConfig,
41 #[serde(default)]
42 pub discord: DiscordConfig,
43 #[serde(default)]
44 pub line: LineConfig,
45 #[serde(default)]
46 pub openai: OpenAiConfig,
47 #[serde(default)]
48 pub http: HttpConfig,
49}
50
51pub fn load(dir: &Path) -> Result<()> {
53 let config_path = dir.join(CONFIG_FILE);
54 let config_def_path = dir.join(CONFIG_DEF_FILE);
55 let config_cur_path = dir.join(CONFIG_CUR_FILE);
56
57 info!("config directory: {}", dir.to_string_lossy());
58 std::fs::create_dir_all(dir)?;
59 {
60 info!("remove {CONFIG_DEF_FILE}");
62 if let Err(e) = remove_file(&config_def_path) {
63 warn!(
64 "removing {} failed (the first time execution?): {e}",
65 config_def_path.to_string_lossy()
66 );
67 }
68 info!("writing default config to {CONFIG_DEF_FILE}");
71 let main_cfg: Config = Default::default();
72 let main_toml = toml::to_string(&main_cfg)?;
73 let mut f = OpenOptions::new()
74 .write(true)
75 .create_new(true)
76 .mode(0o600)
77 .open(config_def_path)
78 .with_context(|| format!("Failed to open {CONFIG_DEF_FILE}"))?;
79 f.write_all(main_toml.as_bytes())
80 .with_context(|| format!("Failed to write {CONFIG_DEF_FILE}"))?;
81 info!("OK: written to {CONFIG_DEF_FILE}");
82 }
84
85 let toml_str = {
86 info!("loading config: {CONFIG_FILE}");
89 let mut f = OpenOptions::new()
90 .read(true)
91 .open(&config_path)
92 .with_context(|| format!("Failed to open {CONFIG_FILE} (the first execution?)"))
93 .with_context(|| {
94 format!("HINT: Copy {CONFIG_DEF_FILE} to {CONFIG_FILE} and try again")
95 })?;
96
97 let metadata = f.metadata()?;
98 let permissions = metadata.permissions();
99 let masked = permissions.mode() & 0o777;
100 ensure!(
101 masked == 0o600,
102 "Config file permission is not 600: {:03o}",
103 permissions.mode()
104 );
105
106 let mut toml_str = String::new();
107 f.read_to_string(&mut toml_str)
108 .with_context(|| format!("Failed to read {CONFIG_FILE}"))?;
109 info!("OK: {CONFIG_FILE} loaded");
110
111 toml_str
112 };
114
115 let mut config = CONFIG.write().unwrap();
117 *config = Some(toml::from_str(&toml_str)?);
118
119 {
120 info!("remove {CONFIG_CUR_FILE}");
122 if let Err(e) = remove_file(&config_cur_path) {
123 warn!("removing {CONFIG_CUR_FILE} failed (the first time execution?): {e}");
124 }
125 info!("writing current config to {CONFIG_CUR_FILE}");
128 let main_toml = toml::to_string(&*config)?;
129 let mut f = OpenOptions::new()
130 .write(true)
131 .create_new(true)
132 .mode(0o600)
133 .open(&config_cur_path)
134 .with_context(|| format!("Failed to open {CONFIG_CUR_FILE}"))?;
135 f.write_all(main_toml.as_bytes())
136 .with_context(|| format!("Failed to write {CONFIG_CUR_FILE}"))?;
137 info!("OK: written to {CONFIG_CUR_FILE}");
138 }
140
141 Ok(())
142}
143
144pub struct ConfigGuard;
145
146impl Drop for ConfigGuard {
147 fn drop(&mut self) {
148 unset();
149 }
150}
151
152#[must_use]
153pub fn set(cfg: Config) -> ConfigGuard {
154 let mut config = CONFIG.write().unwrap();
155 assert!(config.is_none());
156 *config = Some(cfg);
157 ConfigGuard
158}
159
160pub fn unset() {
161 let mut config = CONFIG.write().unwrap();
162 assert!(config.is_some());
163 *config = None;
164}
165
166pub fn get<F, R>(f: F) -> R
167where
168 F: FnOnce(&Config) -> R,
169{
170 let config = CONFIG.read().unwrap();
171 f(config.as_ref().unwrap())
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use serial_test::serial;
178
179 #[test]
180 #[serial(config)]
181 fn test_if() {
182 let _unset = set(Default::default());
183 get(|cfg| println!("{cfg:?}"));
184 }
185}