1use crate::*;
2use anyhow::{Context, Result};
3use chrono::{Local, NaiveTime};
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 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, amount_history_load: 50,
104 db_autodump_interval: 21600000, 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 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>"), ..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}