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::sync::RwLock;
12
13use crate::sysmod::camera::CameraConfig;
14use crate::sysmod::discord::DiscordConfig;
15use crate::sysmod::health::HealthConfig;
16use crate::sysmod::http::HttpConfig;
17use crate::sysmod::line::LineConfig;
18use crate::sysmod::openai::OpenAiConfig;
19use crate::sysmod::twitter::TwitterConfig;
20
21const CONFIG_FILE: &str = "config.toml";
23const CONFIG_DEF_FILE: &str = "config_default.toml";
25const CONFIG_CUR_FILE: &str = "config_current.toml";
27
28static CONFIG: RwLock<Option<Config>> = RwLock::new(None);
30
31#[derive(Debug, Default, Clone, Serialize, Deserialize)]
33pub struct Config {
34 #[serde(default)]
35 pub health: HealthConfig,
36 #[serde(default)]
37 pub camera: CameraConfig,
38 #[serde(default)]
39 pub twitter: TwitterConfig,
40 #[serde(default)]
41 pub discord: DiscordConfig,
42 #[serde(default)]
43 pub line: LineConfig,
44 #[serde(default)]
45 pub openai: OpenAiConfig,
46 #[serde(default)]
47 pub http: HttpConfig,
48}
49
50pub fn load() -> Result<()> {
52 {
53 info!("remove {CONFIG_DEF_FILE}");
55 if let Err(e) = remove_file(CONFIG_DEF_FILE) {
56 warn!("removing {CONFIG_DEF_FILE} failed (the first time execution?): {e}");
57 }
58 info!("writing default config to {CONFIG_DEF_FILE}");
61 let main_cfg: Config = Default::default();
62 let main_toml = toml::to_string(&main_cfg)?;
63 let mut f = OpenOptions::new()
64 .write(true)
65 .create_new(true)
66 .mode(0o600)
67 .open(CONFIG_DEF_FILE)
68 .with_context(|| format!("Failed to open {CONFIG_DEF_FILE}"))?;
69 f.write_all(main_toml.as_bytes())
70 .with_context(|| format!("Failed to write {CONFIG_DEF_FILE}"))?;
71 info!("OK: written to {CONFIG_DEF_FILE}");
72 }
74
75 let toml_str = {
76 info!("loading config: {CONFIG_FILE}");
79 let mut f = OpenOptions::new()
80 .read(true)
81 .open(CONFIG_FILE)
82 .with_context(|| format!("Failed to open {CONFIG_FILE} (the first execution?)"))
83 .with_context(|| {
84 format!("HINT: Copy {CONFIG_DEF_FILE} to {CONFIG_FILE} and try again")
85 })?;
86
87 let metadata = f.metadata()?;
88 let permissions = metadata.permissions();
89 let masked = permissions.mode() & 0o777;
90 ensure!(
91 masked == 0o600,
92 "Config file permission is not 600: {:03o}",
93 permissions.mode()
94 );
95
96 let mut toml_str = String::new();
97 f.read_to_string(&mut toml_str)
98 .with_context(|| format!("Failed to read {CONFIG_FILE}"))?;
99 info!("OK: {CONFIG_FILE} loaded");
100
101 toml_str
102 };
104
105 let mut config = CONFIG.write().unwrap();
107 *config = Some(toml::from_str(&toml_str)?);
108
109 {
110 info!("remove {CONFIG_CUR_FILE}");
112 if let Err(e) = remove_file(CONFIG_CUR_FILE) {
113 warn!("removing {CONFIG_CUR_FILE} failed (the first time execution?): {e}");
114 }
115 info!("writing current config to {CONFIG_CUR_FILE}");
118 let main_toml = toml::to_string(&*config)?;
119 let mut f = OpenOptions::new()
120 .write(true)
121 .create_new(true)
122 .mode(0o600)
123 .open(CONFIG_CUR_FILE)
124 .with_context(|| format!("Failed to open {CONFIG_CUR_FILE}"))?;
125 f.write_all(main_toml.as_bytes())
126 .with_context(|| format!("Failed to write {CONFIG_CUR_FILE}"))?;
127 info!("OK: written to {CONFIG_CUR_FILE}");
128 }
130
131 Ok(())
132}
133
134pub struct ConfigGuard;
135
136impl Drop for ConfigGuard {
137 fn drop(&mut self) {
138 unset();
139 }
140}
141
142#[must_use]
143pub fn set(cfg: Config) -> ConfigGuard {
144 let mut config = CONFIG.write().unwrap();
145 assert!(config.is_none());
146 *config = Some(cfg);
147 ConfigGuard
148}
149
150pub fn unset() {
151 let mut config = CONFIG.write().unwrap();
152 assert!(config.is_some());
153 *config = None;
154}
155
156pub fn get<F, R>(f: F) -> R
157where
158 F: FnOnce(&Config) -> R,
159{
160 let config = CONFIG.read().unwrap();
161 f(config.as_ref().unwrap())
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use serial_test::serial;
168
169 #[test]
170 #[serial(config)]
171 fn test_if() {
172 let _unset = set(Default::default());
173 get(|cfg| println!("{cfg:?}"));
174 }
175}