use std::env; use std::net::{IpAddr, Ipv6Addr}; use std::path::PathBuf; use color_eyre::eyre::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tracing::{error, info, instrument}; use url::Url; use crate::ranged_i128_visitor::RangedI128Visitor; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(default)] pub struct SyntectConfig { pub load_defaults: bool, pub themes_dir: Option, pub theme: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, Default)] #[serde(default)] pub struct RenderConfig { pub syntect: SyntectConfig, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct CacheConfig { pub enable: bool, pub cleanup: bool, pub cleanup_interval: Option, pub persistence: bool, pub file: PathBuf, pub compress: bool, #[serde(deserialize_with = "check_zstd_level_bounds")] pub compression_level: i32, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct HttpConfig { pub host: IpAddr, pub port: u16, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct DirsConfig { pub posts: PathBuf, pub media: PathBuf, #[serde(rename = "static")] pub _static: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RssConfig { pub enable: bool, pub link: Url, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub enum DateFormat { #[default] RFC3339, #[serde(untagged)] Strftime(String), } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Config { pub title: String, pub description: String, pub markdown_access: bool, pub date_format: DateFormat, pub js_enable: bool, pub rss: RssConfig, pub dirs: DirsConfig, pub http: HttpConfig, pub render: RenderConfig, pub cache: CacheConfig, } impl Default for Config { fn default() -> Self { Self { title: "bingus-blog".into(), description: "blazingly fast markdown blog software written in rust memory safe".into(), markdown_access: true, date_format: Default::default(), js_enable: true, // i have a love-hate relationship with serde // it was engimatic at first, but then i started actually using it // writing my own serialize and deserialize implementations.. spending // a lot of time in the docs trying to understand each and every option.. // now with this knowledge i can do stuff like this! (see rss field) // and i'm proud to say that it still makes 0 sense. rss: RssConfig { enable: false, link: Url::parse("http://example.com").unwrap(), }, dirs: Default::default(), http: Default::default(), render: Default::default(), cache: Default::default(), } } } impl Default for DirsConfig { fn default() -> Self { Self { posts: "posts".into(), media: "media".into(), _static: "static".into(), } } } impl Default for HttpConfig { fn default() -> Self { Self { host: IpAddr::V6(Ipv6Addr::UNSPECIFIED), port: 3000, } } } impl Default for SyntectConfig { fn default() -> Self { Self { load_defaults: false, themes_dir: Some("themes".into()), theme: Some("Catppuccin Mocha".into()), } } } impl Default for CacheConfig { fn default() -> Self { Self { enable: true, cleanup: true, cleanup_interval: None, persistence: true, file: "cache".into(), compress: true, compression_level: 3, } } } #[instrument(name = "config")] pub async fn load() -> Result { let config_file = env::var(format!( "{}_CONFIG", env!("CARGO_BIN_NAME").to_uppercase().replace("-", "_") )) .unwrap_or(String::from("config.toml")); match tokio::fs::OpenOptions::new() .read(true) .open(&config_file) .await { Ok(mut file) => { let mut buf = String::new(); file.read_to_string(&mut buf) .await .context("couldn't read configuration file")?; toml::from_str(&buf).context("couldn't parse configuration") } Err(err) => match err.kind() { std::io::ErrorKind::NotFound => { let config = Config::default(); info!("configuration file doesn't exist, creating"); match tokio::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&config_file) .await { Ok(mut file) => file .write_all( toml::to_string_pretty(&config) .context("couldn't serialize configuration")? .as_bytes(), ) .await .unwrap_or_else(|err| error!("couldn't write configuration: {}", err)), Err(err) => { error!("couldn't open file {:?} for writing: {}", &config_file, err) } } Ok(config) } _ => bail!("couldn't open config file: {}", err), }, } } fn check_zstd_level_bounds<'de, D>(d: D) -> Result where D: serde::Deserializer<'de>, { d.deserialize_i32(RangedI128Visitor::<1, 22>) .map(|x| x as i32) }