use clap::ValueEnum; use std::{fmt::Display, path::PathBuf, str::FromStr}; use tracing::Level; use url::Url; #[derive( Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, ValueEnum, )] #[serde(rename_all = "snake_case")] pub(crate) enum LogFormat { Compact, Json, Normal, Pretty, } #[derive(Clone, Debug)] pub(crate) struct Targets { pub(crate) targets: tracing_subscriber::filter::Targets, } /// Configuration for filesystem media storage #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, clap::Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct Filesystem { /// Path to store media #[arg(short, long)] pub(crate) path: PathBuf, } /// Configuration for object media storage #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, clap::Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct ObjectStorage { /// The base endpoint for the object storage /// /// Examples: /// - `http://localhost:9000` /// - `https://s3.dualstack.eu-west-1.amazonaws.com` #[arg(short, long)] pub(crate) endpoint: Url, /// Determines whether to use path style or virtualhost style for accessing objects /// /// When this is true, objects will be fetched from {endpoint}/{bucket_name}/{object} /// When false, objects will be fetched from {bucket_name}.{endpoint}/{object} #[arg(short, long)] pub(crate) use_path_style: bool, /// The bucket in which to store media #[arg(short, long)] pub(crate) bucket_name: String, /// The region the bucket is located in #[arg(short, long)] pub(crate) region: String, /// The Access Key for the user accessing the bucket #[arg(short, long)] pub(crate) access_key: String, /// The secret key for the user accessing the bucket #[arg(short, long)] pub(crate) secret_key: String, /// The session token for accessing the bucket #[arg(long)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) session_token: Option, /// How long signatures for object storage requests are valid (in seconds) /// /// This defaults to 15 seconds #[arg(long)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) signature_duration: Option, /// How long a client can wait on an object storage request before giving up (in seconds) /// /// This defaults to 30 seconds #[arg(long)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) client_timeout: Option, /// Base endpoint at which object storage images are publicly accessible #[arg(long)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) public_endpoint: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] #[serde(tag = "type")] // allow large enum variant - this is an instantiated-once config #[allow(clippy::large_enum_variant)] pub(crate) enum Store { Filesystem(Filesystem), ObjectStorage(ObjectStorage), } impl From for Store { fn from(f: Filesystem) -> Self { Self::Filesystem(f) } } impl From for Store { fn from(o: ObjectStorage) -> Self { Self::ObjectStorage(o) } } impl FromStr for Targets { type Err = ::Err; fn from_str(s: &str) -> Result { Ok(Targets { targets: s.parse()?, }) } } impl Display for Targets { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let targets = self .targets .iter() .map(|(path, level)| format!("{path}={level}")) .collect::>() .join(","); let max_level = [ Level::TRACE, Level::DEBUG, Level::INFO, Level::WARN, Level::ERROR, ] .iter() .fold(None, |found, level| { if found.is_none() && self .targets .would_enable("not_a_real_target_so_nothing_can_conflict", level) { Some(level.to_string().to_lowercase()) } else { found } }); if let Some(level) = max_level { if !targets.is_empty() { write!(f, "{level},{targets}") } else { write!(f, "{level}") } } else if !targets.is_empty() { write!(f, "{targets}") } else { Ok(()) } } } impl FromStr for LogFormat { type Err = String; fn from_str(s: &str) -> Result { for variant in Self::value_variants() { if variant.to_possible_value().unwrap().matches(s, false) { return Ok(*variant); } } Err(format!("Invalid variant: {s}")) } } impl Display for LogFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } #[cfg(test)] mod tests { use super::Targets; use crate::serde_str::Serde; #[test] fn builds_info_targets() { let t: Serde = "info".parse().unwrap(); println!("{:?}", t); assert_eq!(t.to_string(), "info"); } #[test] fn builds_specific_targets() { let t: Serde = "pict_rs=info".parse().unwrap(); assert_eq!(t.to_string(), "pict_rs=info"); } #[test] fn builds_warn_and_specific_targets() { let t: Serde = "warn,pict_rs=info".parse().unwrap(); assert_eq!(t.to_string(), "warn,pict_rs=info"); } }