From 4ee286e7c64c8a7fbcc14daa8b175dafb1091a80 Mon Sep 17 00:00:00 2001 From: Adrian Wannenmacher Date: Tue, 27 Jun 2023 18:02:49 +0200 Subject: [PATCH] add configuration --- Cargo.toml | 3 +- src/config.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 54 ++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 src/config.rs diff --git a/Cargo.toml b/Cargo.toml index 24e6a7a..4c97edf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" [dependencies] anyhow = "1.0.71" -clap = { version = "4.3.8", features = ["derive", "env"] } +clap = { version = "4.3.8", features = ["cargo", "derive", "env", "wrap_help"] } +directories = "5.0.1" downloader = { version = "0.2.7", default-features = false, features = ["rustls-tls"] } serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.99" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..33b3203 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,107 @@ +use std::{ + fmt::Display, + fs::File, + io::{BufReader, Read}, + path::{Path, PathBuf}, + time::Duration, +}; + +use anyhow::{anyhow, Context}; +use downloader::Downloader; +use serde::{Deserialize, Serialize}; + +use crate::{PROJ_DIRS, USER_DIRS}; + +/// Global configuration values. +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + /// The base directory used for resolving relative paths. Defaults to the home directory. + pub base_directory: PathBuf, + /// The number of parallel downloads. Defaults to 32. + pub parallel_downloads: u16, + /// The number of retries. Defaults to 3. + pub retries: u16, + /// The timeout for establishing a connection in seconds. Defaults to 15. + pub timeout_connection: u64, + /// The timeout for downloading a file in seconds. Defaults to 30. + pub timeout_download: u64, + /// The user agent to use for HTTP communication. Default is not stabilized. + pub user_agent: String, +} + +impl Config { + /// Read the configuration from the default file, or revert to default values if unavailable. + pub fn read_from_default_file() -> anyhow::Result { + let dirs = PROJ_DIRS.get().expect("directories not initialized"); + + let mut path = dirs.config_dir().to_path_buf(); + path.push("config.toml"); + + if path.is_file() { + Self::read_from_file(&path) + } else if !path.exists() { + Ok(Self::default()) + } else { + Err(anyhow!( + "default configuration file is neither file nor nonexistent" + )) + } + } + + /// Read the configuration from the specified file. + pub fn read_from_file(path: &Path) -> anyhow::Result { + let file = File::open(path).context("failed to open config file")?; + let mut buf = BufReader::new(file); + + let mut data = String::new(); + buf.read_to_string(&mut data) + .context("failed to read config data")?; + + toml::from_str(&data).context("failed to parse config file") + } + + /// Create a [`Downloader`] with the configuration applied. + pub fn downloader(&self) -> anyhow::Result { + Downloader::builder() + .user_agent(&self.user_agent) + .connect_timeout(Duration::from_secs(self.timeout_connection)) + .timeout(Duration::from_secs(self.timeout_download)) + .parallel_requests(self.parallel_downloads) + .retries(self.retries) + .download_folder(&self.base_directory) + .build() + .context("failed to build downloader") + } +} + +impl Default for Config { + fn default() -> Self { + let dirs = USER_DIRS.get().expect("directories not initialized"); + + Self { + base_directory: dirs.home_dir().to_path_buf(), + parallel_downloads: 32, + retries: 3, + timeout_connection: 15, + timeout_download: 30, + user_agent: format!( + "{} {}.{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION_MAJOR"), + env!("CARGO_PKG_VERSION_MINOR") + ), + } + } +} + +impl Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "base directory: {}", self.base_directory.display())?; + writeln!(f, "parallel downloads: {}", self.parallel_downloads)?; + writeln!(f, "retries: {}", self.retries)?; + writeln!(f, "timeout connection: {}s", self.timeout_connection)?; + writeln!(f, "timeout download: {}s", self.timeout_download)?; + write!(f, "user agent: {}", self.user_agent) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..4f7c081 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,53 @@ -fn main() { - println!("Hello, world!"); +use std::sync::OnceLock; + +use anyhow::Context; +use clap::{Parser, Subcommand}; +use config::Config; +use directories::{ProjectDirs, UserDirs}; + +mod config; + +static USER_DIRS: OnceLock = OnceLock::new(); +static PROJ_DIRS: OnceLock = OnceLock::new(); + +fn main() -> anyhow::Result<()> { + let cli = CLI::parse(); + + // initialize dirs + let user_dirs = UserDirs::new().context("failed to discover user directiories")?; + let proj_dirs = ProjectDirs::from("dev", "TFLD", "ListLoad") + .context("failed to discover program directories")?; + + USER_DIRS + .set(user_dirs) + .ok() + .context("failed to initialize user directories")?; + PROJ_DIRS + .set(proj_dirs) + .ok() + .context("failed to initialize program directories")?; + + // initialize downloading + let cfg = Config::read_from_default_file().context("failed to load config")?; + let downloader = cfg.downloader().context("failed to create downloader")?; + + match cli.command { + CMD::Config => { + println!("{cfg}"); + Ok(()) + } + } +} + +#[derive(Parser)] +#[clap(about, author, version)] +struct CLI { + #[clap(subcommand)] + command: CMD, +} + +#[derive(Subcommand)] +enum CMD { + /// Print out the current configuration. + Config, }