small_bin/
config.rs

1use crate::*;
2use anyhow::{Context, Result};
3use chrono::{Local, NaiveTime};
4use serde::{Deserialize, Serialize};
5use std::{fs, path::PathBuf};
6
7
8/// Multiple configurations for sync
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Configs {
11    pub configs: Vec<Config>,
12    pub notifications: NotificationSettings,
13    pub sounds: SoundSettings,
14    pub open_history_on_start: bool,
15}
16
17/// A single configuration entry
18#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
19pub struct Config {
20    pub username: String,
21    pub hostname: String,
22    pub ssh_key: String,
23    pub ssh_port: u16,
24    pub address: String,
25    pub remote_path: String,
26    pub ssh_key_pass: String,
27    pub watch_path: String,
28    pub active_at: String, // hour range when to activate it: example: "9:01:00-15:55:00"
29    pub default: bool,
30}
31
32
33#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default)]
34pub struct NotificationSettings {
35    pub start: bool,
36    pub clipboard: bool,
37    pub upload: bool,
38    pub error: bool,
39}
40
41
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct SoundSettings {
44    pub start: bool,
45    pub start_sound: String,
46    pub clipboard: bool,
47    pub clipboard_sound: String,
48    pub upload: bool,
49    pub upload_sound: String,
50    pub error: bool,
51    pub error_sound: String,
52}
53
54
55#[derive(Debug, Clone, Default)]
56pub struct AppConfig {
57    pub configs: Vec<Config>,
58    pub notifications: NotificationSettings,
59    pub sounds: SoundSettings,
60    pub open_history_on_start: bool,
61    pub env: String,
62    pub fs_check_interval: u64,
63    pub amount_history_load: usize,
64    pub db_autodump_interval: u64,
65    pub ssh_connection_timeout: u64,
66    pub sftp_buffer_size: usize,
67    pub webapi_port: u16,
68}
69
70
71impl AppConfig {
72    pub fn new() -> Result<Self> {
73        let env = std::env::var("ENV").unwrap_or_else(|_| "prod".to_string());
74        let config_file = Self::default_config_file();
75
76        if !config_file.exists() {
77            anyhow::bail!("No configuration file: {:?}", config_file);
78        }
79
80        let config_content = fs::read_to_string(&config_file)
81            .context(format!("Cannot open config file: {:?}", config_file))?;
82
83        let config: Configs =
84            toml::from_str(&config_content).context("Failed to parse config file")?;
85
86        for config in &config.configs {
87            Self::validate_config(config)?;
88        }
89
90        let webapi_port = match env.as_str() {
91            "dev" => 8001,
92            "test" => 8002,
93            _ => 8000,
94        };
95
96        Ok(AppConfig {
97            configs: config.configs.clone(),
98            notifications: config.notifications,
99            sounds: config.sounds,
100            env,
101            open_history_on_start: config.open_history_on_start,
102            fs_check_interval: 1000, // ms
103            amount_history_load: 50,
104            db_autodump_interval: 21600000, // 6 hours in ms
105            ssh_connection_timeout: 30000,
106            sftp_buffer_size: 262144,
107            webapi_port,
108        })
109    }
110
111
112    fn validate_config(config: &Config) -> Result<()> {
113        if config.username.is_empty() {
114            anyhow::bail!("Required configuration value: username is empty!");
115        }
116        if config.hostname.is_empty() {
117            anyhow::bail!("Required configuration value: hostname is empty!");
118        }
119        if config.ssh_port == 0 {
120            anyhow::bail!("Required configuration value: ssh_port is zero!");
121        }
122        if config.address.is_empty() {
123            anyhow::bail!("Required configuration value: address is empty!");
124        }
125        if config.remote_path.is_empty() {
126            anyhow::bail!("Required configuration value: remote_path is empty!");
127        }
128        Ok(())
129    }
130
131
132    pub fn data_dir_base() -> &'static str {
133        if cfg!(target_os = "macos") {
134            "/Library/Small/"
135        } else {
136            "/.small/"
137        }
138    }
139
140
141    // selects the current config based on the time
142    pub fn select_config(&self) -> Result<Config> {
143        let now = Local::now().time();
144
145        let configs = &self.configs;
146        let config = configs.iter().find(|cfg| {
147            let range: Vec<&str> = cfg.active_at.split('-').collect();
148            if range.len() > 2 {
149                error!("Wrong format of the time range. Should be: HH:MM:SS-HH:MM:SS");
150                return false;
151            }
152            let time_start = NaiveTime::parse_from_str(range[0], "%H:%M:%S")
153                .expect("Valid time is expected");
154            let time_end = NaiveTime::parse_from_str(range[1], "%H:%M:%S")
155                .expect("Valid time is expected");
156
157            now >= time_start && now <= time_end
158        });
159
160        let default_config = configs
161            .iter()
162            .find(|cfg| cfg.default)
163            .expect("One of configs has to be the default!");
164
165        if let Some(cfg) = config {
166            debug!(
167                "Selected config: {configuration:?}",
168                configuration = Config {
169                    ssh_key_pass: String::from("<redacted>"), /* don't print the ssh key in logs */
170                    ..cfg.clone()
171                }
172            );
173        }
174
175        match config {
176            Some(cfg) => Ok(cfg.clone()),
177            None => {
178                debug!(
179                    "No config to select by the active_at range. Selecting the default one."
180                );
181                Ok(default_config.clone())
182            }
183        }
184    }
185
186
187    pub fn project_root_dir() -> PathBuf {
188        let home = home::home_dir().expect("Could not determine home directory");
189        home.join(Self::data_dir_base().trim_start_matches('/'))
190    }
191
192
193    pub fn project_dir(&self) -> PathBuf {
194        Self::project_root_dir().join(&self.env)
195    }
196
197
198    pub fn db_dumps_dir(&self) -> PathBuf {
199        Self::project_root_dir().join(format!(".sqlite-dumps-{}", self.env))
200    }
201
202
203    pub fn default_config_file() -> PathBuf {
204        Self::project_root_dir().join("config.toml")
205    }
206
207
208    pub fn database_path(&self) -> PathBuf {
209        self.project_dir().join("small.db")
210    }
211}