use std::fs;
use std::path::PathBuf;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::APP_NAME;
fn default_download_dir() -> PathBuf {
dirs::download_dir().unwrap_or_else(|| dirs::home_dir().expect("home dir"))
}
fn default_state_dir() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| dirs::home_dir().expect("home dir"))
.join(APP_NAME)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub download_dir: PathBuf,
pub state_dir: PathBuf,
pub trackers: Vec<String>,
pub upload_bps: Option<u32>,
pub download_bps: Option<u32>,
pub socks_proxy: Option<String>,
pub auth_token: String,
pub api_port: u16,
pub watch_dirs: Vec<PathBuf>,
pub library_dirs: Vec<PathBuf>,
pub schedule: Vec<ScheduleEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduleEntry {
pub start: String,
pub end: String,
pub upload_bps: Option<u32>,
pub download_bps: Option<u32>,
}
impl Default for Config {
fn default() -> Self {
Self {
download_dir: default_download_dir(),
state_dir: default_state_dir(),
trackers: Vec::new(),
upload_bps: None,
download_bps: None,
socks_proxy: None,
auth_token: generate_token(),
api_port: 8170,
watch_dirs: Vec::new(),
library_dirs: Vec::new(),
schedule: Vec::new(),
}
}
}
impl Config {
pub fn config_file() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| dirs::home_dir().expect("home dir"))
.join(APP_NAME)
.join("config.toml")
}
pub fn load() -> anyhow::Result<Self> {
let path = Self::config_file();
let cfg = match fs::read_to_string(&path) {
Ok(raw) => toml::from_str::<Self>(&raw)
.with_context(|| format!("parsing {}", path.display()))?,
Err(_) => {
let cfg = Self::default();
cfg.save()?;
cfg
}
};
fs::create_dir_all(&cfg.state_dir)
.with_context(|| format!("creating state dir {}", cfg.state_dir.display()))?;
Ok(cfg)
}
pub fn save(&self) -> anyhow::Result<()> {
let path = Self::config_file();
fs::create_dir_all(path.parent().expect("config parent"))
.with_context(|| format!("creating config dir {}", path.display()))?;
let raw = toml::to_string_pretty(self).context("serializing config")?;
fs::write(&path, raw).with_context(|| format!("writing {}", path.display()))
}
}
fn generate_token() -> String {
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes).expect("OS randomness");
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_roundtrips() {
let cfg = Config::default();
let raw = toml::to_string(&cfg).unwrap();
let back: Config = toml::from_str(&raw).unwrap();
assert_eq!(back.auth_token, cfg.auth_token);
assert_eq!(back.download_dir, cfg.download_dir);
}
#[test]
fn missing_token_is_generated() {
let raw = "download_dir = \"/tmp/dl\"\n";
let cfg: Config = toml::from_str(raw).unwrap();
assert_eq!(cfg.auth_token.len(), 32);
assert_ne!(cfg.auth_token, Config::default().auth_token);
}
}