use crate::*;
use anyhow::{Context, Result};
use chrono::{Datelike, Local, NaiveTime, Weekday};
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Configs {
pub configs: Vec<Config>,
pub notifications: NotificationSettings,
pub sounds: SoundSettings,
pub open_history_on_start: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct Config {
pub username: String,
pub hostname: String,
pub ssh_key: String,
pub ssh_port: u16,
pub address: String,
pub remote_path: String,
pub ssh_key_pass: String,
pub watch_path: String,
pub active_at: String, pub active_on: Vec<Weekday>, pub default: bool,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default)]
pub struct NotificationSettings {
pub start: bool,
pub clipboard: bool,
pub upload: bool,
pub error: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SoundSettings {
pub start: bool,
pub start_sound: String,
pub clipboard: bool,
pub clipboard_sound: String,
pub upload: bool,
pub upload_sound: String,
pub error: bool,
pub error_sound: String,
}
#[derive(Debug, Clone, Default)]
pub struct AppConfig {
pub configs: Vec<Config>,
pub notifications: NotificationSettings,
pub sounds: SoundSettings,
pub open_history_on_start: bool,
pub env: String,
pub fs_check_interval: u64,
pub amount_history_load: usize,
pub db_autodump_interval: u64,
pub ssh_connection_timeout: u64,
pub sftp_buffer_size: usize,
pub webapi_port: u16,
}
impl AppConfig {
pub fn new() -> Result<Self> {
let env = std::env::var("ENV").unwrap_or_else(|_| "prod".to_string());
let config_file = Self::default_config_file();
if !config_file.exists() {
anyhow::bail!("No configuration file: {:?}", config_file);
}
let config_content = fs::read_to_string(&config_file)
.context(format!("Cannot open config file: {:?}", config_file))?;
let config: Configs =
toml::from_str(&config_content).context("Failed to parse config file")?;
for config in &config.configs {
Self::validate_config(config)?;
}
let webapi_port = match env.as_str() {
"dev" => 8001,
"test" => 8002,
_ => 8000,
};
Ok(AppConfig {
configs: config.configs.clone(),
notifications: config.notifications,
sounds: config.sounds,
env,
open_history_on_start: config.open_history_on_start,
fs_check_interval: 1000, amount_history_load: 50,
db_autodump_interval: 21600000, ssh_connection_timeout: 30000,
sftp_buffer_size: 262144,
webapi_port,
})
}
fn validate_config(config: &Config) -> Result<()> {
if config.username.is_empty() {
anyhow::bail!("Required configuration value: username is empty!");
}
if config.hostname.is_empty() {
anyhow::bail!("Required configuration value: hostname is empty!");
}
if config.ssh_port == 0 {
anyhow::bail!("Required configuration value: ssh_port is zero!");
}
if config.address.is_empty() {
anyhow::bail!("Required configuration value: address is empty!");
}
if config.remote_path.is_empty() {
anyhow::bail!("Required configuration value: remote_path is empty!");
}
Ok(())
}
pub fn data_dir_base() -> &'static str {
if cfg!(target_os = "macos") {
"/Library/Small/"
} else {
"/.small/"
}
}
pub fn select_config(&self) -> Result<Config> {
let now = Local::now();
let time = now.time();
let weekday = now.weekday();
let configs = &self.configs;
let config = configs.iter().find(|cfg| {
if !cfg.active_on.is_empty() && !cfg.active_on.contains(&weekday) {
debug!("Active_on doesn't contain the current day: {weekday}");
return false;
}
if cfg.active_at.is_empty() {
debug!("Active_at is empty and thee current day is {weekday}, meaning the config is valid for the whole day");
return true;
}
let range: Vec<&str> = cfg.active_at.split('-').collect();
if range.len() != 2 {
error!("Wrong format of the time range. Should be: HH:MM:SS-HH:MM:SS");
return false;
}
let time_start = NaiveTime::parse_from_str(range[0], "%H:%M:%S")
.expect("Valid time is expected");
let time_end = NaiveTime::parse_from_str(range[1], "%H:%M:%S")
.expect("Valid time is expected");
time >= time_start && time <= time_end
});
let default_config = configs
.iter()
.find(|cfg| cfg.default)
.expect("One of configs has to be the default!");
if let Some(cfg) = config {
debug!(
"Selected config: {configuration:?}",
configuration = Config {
ssh_key_pass: String::from("<redacted>"),
..cfg.clone()
}
);
}
match config {
Some(cfg) => Ok(cfg.clone()),
None => {
debug!(
"No config to select by the active_at range. Selecting the default one."
);
Ok(default_config.clone())
}
}
}
pub fn project_root_dir() -> PathBuf {
let home = home::home_dir().expect("Could not determine home directory");
home.join(Self::data_dir_base().trim_start_matches('/'))
}
pub fn project_dir(&self) -> PathBuf {
Self::project_root_dir().join(&self.env)
}
pub fn db_dumps_dir(&self) -> PathBuf {
Self::project_root_dir().join(format!(".sqlite-dumps-{}", self.env))
}
pub fn default_config_file() -> PathBuf {
Self::project_root_dir().join("config.toml")
}
pub fn database_path(&self) -> PathBuf {
self.project_dir().join("small.db")
}
}