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