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::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
21/// ロードする設定ファイルパス。
22const CONFIG_FILE: &str = "config.toml";
23/// デフォルト設定の出力パス。
24const CONFIG_DEF_FILE: &str = "config_default.toml";
25/// 現在設定の出力パス。
26const CONFIG_CUR_FILE: &str = "config_current.toml";
27
28/// 設定データ(グローバル変数)。
29static CONFIG: RwLock<Option<Config>> = RwLock::new(None);
30
31/// 設定データ。
32#[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
50/// 設定データをロードする。
51pub fn load() -> Result<()> {
52    {
53        // デフォルト設定ファイルを削除する
54        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        // デフォルト設定を書き出す
59        // permission=600 でアトミックに必ず新規作成する、失敗したらエラー
60        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        // close
73    }
74
75    let toml_str = {
76        // 設定ファイルを読む
77        // open 後パーミッションを確認し、危険ならエラーとする
78        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        // close f
103    };
104
105    // グローバル変数に設定する
106    let mut config = CONFIG.write().unwrap();
107    *config = Some(toml::from_str(&toml_str)?);
108
109    {
110        // 現在設定ファイルを削除する
111        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        // 現在設定を書き出す
116        // permission=600 でアトミックに必ず新規作成する、失敗したらエラー
117        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        // close
129    }
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}