use std::{collections::HashSet, net::SocketAddr, path::PathBuf}; use structopt::StructOpt; use url::Url; use crate::magick::ValidInputType; #[derive(Clone, Debug, StructOpt)] pub(crate) struct Args { #[structopt(short, long, help = "Path to the pict-rs configuration file")] config_file: Option, #[structopt(long, help = "Path to a file defining a store migration")] migrate_file: Option, #[structopt(flatten)] overrides: Overrides, } fn is_false(b: &bool) -> bool { !b } #[derive(Clone, Debug, serde::Serialize, structopt::StructOpt)] #[serde(rename_all = "snake_case")] pub(crate) struct Overrides { #[structopt( short, long, help = "Whether to skip validating images uploaded via the internal import API" )] #[serde(skip_serializing_if = "is_false")] skip_validate_imports: bool, #[structopt(short, long, help = "The address and port the server binds to.")] #[serde(skip_serializing_if = "Option::is_none")] addr: Option, #[structopt(short, long, help = "The path to the data directory, e.g. data/")] #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[structopt( short, long, help = "An optional image format to convert all uploaded files into, supports 'jpg', 'png', and 'webp'" )] #[serde(skip_serializing_if = "Option::is_none")] image_format: Option, #[structopt( short, long, help = "An optional list of filters to permit, supports 'identity', 'thumbnail', 'resize', 'crop', and 'blur'" )] #[serde(skip_serializing_if = "Option::is_none")] filters: Option>, #[structopt( short, long, help = "Specify the maximum allowed uploaded file size (in Megabytes)" )] #[serde(skip_serializing_if = "Option::is_none")] max_file_size: Option, #[structopt(long, help = "Specify the maximum width in pixels allowed on an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_width: Option, #[structopt(long, help = "Specify the maximum width in pixels allowed on an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_height: Option, #[structopt(long, help = "Specify the maximum area in pixels allowed in an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_area: Option, #[structopt( long, help = "An optional string to be checked on requests to privileged endpoints" )] #[serde(skip_serializing_if = "Option::is_none")] api_key: Option, #[structopt( short, long, help = "Enable OpenTelemetry Tracing exports to the given OpenTelemetry collector" )] #[serde(skip_serializing_if = "Option::is_none")] opentelemetry_url: Option, #[structopt(subcommand)] #[serde(skip_serializing_if = "Option::is_none")] store: Option, } impl Overrides { fn is_default(&self) -> bool { !self.skip_validate_imports && self.addr.is_none() && self.path.is_none() && self.image_format.is_none() && self.filters.is_none() && self.max_file_size.is_none() && self.max_image_width.is_none() && self.max_image_height.is_none() && self.max_image_area.is_none() && self.api_key.is_none() && self.opentelemetry_url.is_none() && self.store.is_none() } } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub(crate) struct Migrate { from: Store, to: Store, } impl Migrate { pub(crate) fn from(&self) -> &Store { &self.from } pub(crate) fn to(&self) -> &Store { &self.to } } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, structopt::StructOpt)] #[serde(rename_all = "snake_case")] #[serde(tag = "type")] pub(crate) enum Store { FileStore { // defaults to {config.path} #[structopt( long, help = "Path in which pict-rs will create it's 'files' directory" )] #[serde(skip_serializing_if = "Option::is_none")] path: Option, }, #[cfg(feature = "object-storage")] S3Store { #[structopt(long, help = "Name of the bucket in which pict-rs will store images")] bucket_name: String, #[structopt( long, help = "Region in which the bucket exists, can be an http endpoint" )] region: crate::serde_str::Serde, #[serde(skip_serializing_if = "Option::is_none")] #[structopt(long)] access_key: Option, #[structopt(long)] #[serde(skip_serializing_if = "Option::is_none")] secret_key: Option, #[structopt(long)] #[serde(skip_serializing_if = "Option::is_none")] security_token: Option, #[structopt(long)] #[serde(skip_serializing_if = "Option::is_none")] session_token: Option, }, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub(crate) struct Config { skip_validate_imports: bool, addr: SocketAddr, path: PathBuf, image_format: Option, filters: Option>, max_file_size: usize, max_image_width: usize, max_image_height: usize, max_image_area: usize, api_key: Option, opentelemetry_url: Option, store: Store, } #[derive(serde::Serialize)] #[serde(rename_all = "snake_case")] pub(crate) struct Defaults { skip_validate_imports: bool, addr: SocketAddr, max_file_size: usize, max_image_width: usize, max_image_height: usize, max_image_area: usize, store: Store, } impl Defaults { fn new() -> Self { Defaults { skip_validate_imports: false, addr: ([0, 0, 0, 0], 8080).into(), max_file_size: 40, max_image_width: 10_000, max_image_height: 10_000, max_image_area: 40_000_000, store: Store::FileStore { path: None }, } } } impl Config { pub(crate) fn build() -> anyhow::Result { let args = Args::from_args(); let mut base_config = config::Config::new(); base_config.merge(config::Config::try_from(&Defaults::new())?)?; if let Some(path) = args.migrate_file { let mut migrate_config = config::Config::new(); migrate_config.merge(config::File::from(path))?; let migrate: Migrate = migrate_config.try_into()?; crate::MIGRATE.set(migrate).unwrap(); } if let Some(path) = args.config_file { base_config.merge(config::File::from(path))?; }; if !args.overrides.is_default() { let merging = config::Config::try_from(&args.overrides)?; base_config.merge(merging)?; } base_config.merge(config::Environment::with_prefix("PICTRS").separator("__"))?; let config: Self = base_config.try_into()?; Ok(config) } pub(crate) fn store(&self) -> &Store { &self.store } pub(crate) fn bind_address(&self) -> SocketAddr { self.addr } pub(crate) fn data_dir(&self) -> PathBuf { self.path.clone() } pub(crate) fn format(&self) -> Option { self.image_format } pub(crate) fn allowed_filters(&self) -> Option> { self.filters.as_ref().map(|wl| wl.iter().cloned().collect()) } pub(crate) fn validate_imports(&self) -> bool { !self.skip_validate_imports } pub(crate) fn max_file_size(&self) -> usize { self.max_file_size } pub(crate) fn max_width(&self) -> usize { self.max_image_width } pub(crate) fn max_height(&self) -> usize { self.max_image_height } pub(crate) fn max_area(&self) -> usize { self.max_image_area } pub(crate) fn api_key(&self) -> Option<&str> { self.api_key.as_deref() } pub(crate) fn opentelemetry_url(&self) -> Option<&Url> { self.opentelemetry_url.as_ref() } } #[derive(Debug, thiserror::Error)] #[error("Invalid format supplied, {0}")] pub(crate) struct FormatError(String); #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub(crate) enum Format { Jpeg, Png, Webp, } impl Format { pub(crate) fn as_magick_format(&self) -> &'static str { match self { Format::Jpeg => "JPEG", Format::Png => "PNG", Format::Webp => "WEBP", } } pub(crate) fn as_hint(&self) -> Option { match self { Format::Jpeg => Some(ValidInputType::Jpeg), Format::Png => Some(ValidInputType::Png), Format::Webp => Some(ValidInputType::Webp), } } } impl std::str::FromStr for Format { type Err = FormatError; fn from_str(s: &str) -> Result { match s { "png" => Ok(Format::Png), "jpg" => Ok(Format::Jpeg), "webp" => Ok(Format::Webp), other => Err(FormatError(other.to_string())), } } }