sys/
config.rs

1//! 設定データの管理。
2
3use 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
22/// ロードする設定ファイル。
23const CONFIG_FILE: &str = "config.toml";
24/// デフォルト設定の出力ファイル名。
25const CONFIG_DEF_FILE: &str = "config_default.toml";
26/// 現在設定の出力ファイル名。
27const CONFIG_CUR_FILE: &str = "config_current.toml";
28
29/// 設定データ(グローバル変数)。
30static CONFIG: RwLock<Option<Config>> = RwLock::new(None);
31
32/// 設定データ。
33#[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
51/// 設定データをロードする。
52pub 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        // デフォルト設定ファイルを削除する
61        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        // デフォルト設定を書き出す
69        // permission=600 でアトミックに必ず新規作成する、失敗したらエラー
70        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        // close
83    }
84
85    let toml_str = {
86        // 設定ファイルを読む
87        // open 後パーミッションを確認し、危険ならエラーとする
88        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        // close f
113    };
114
115    // グローバル変数に設定する
116    let mut config = CONFIG.write().unwrap();
117    *config = Some(toml::from_str(&toml_str)?);
118
119    {
120        // 現在設定ファイルを削除する
121        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        // 現在設定を書き出す
126        // permission=600 でアトミックに必ず新規作成する、失敗したらエラー
127        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        // close
139    }
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}