use crate::serde_str::Serde; use clap::{ArgEnum, Parser, Subcommand}; use std::{collections::HashSet, net::SocketAddr, path::PathBuf}; use url::Url; use crate::magick::ValidInputType; #[derive(Clone, Debug, Parser)] pub(crate) struct Args { #[clap(short, long, help = "Path to the pict-rs configuration file")] config_file: Option, #[clap(subcommand)] command: Command, #[clap(flatten)] overrides: Overrides, } fn is_false(b: &bool) -> bool { !b } #[derive(Clone, Debug, serde::Serialize, Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct Overrides { #[clap( short, long, help = "Whether to skip validating images uploaded via the internal import API" )] #[serde(skip_serializing_if = "is_false")] skip_validate_imports: bool, #[clap(short, long, help = "The address and port the server binds to.")] #[serde(skip_serializing_if = "Option::is_none")] addr: Option, #[clap(short, long, help = "The path to the data directory, e.g. data/")] #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[clap( 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, #[clap( 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>, #[clap( short, long, help = "Specify the maximum allowed uploaded file size (in Megabytes)" )] #[serde(skip_serializing_if = "Option::is_none")] max_file_size: Option, #[clap(long, help = "Specify the maximum width in pixels allowed on an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_width: Option, #[clap(long, help = "Specify the maximum width in pixels allowed on an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_height: Option, #[clap(long, help = "Specify the maximum area in pixels allowed in an image")] #[serde(skip_serializing_if = "Option::is_none")] max_image_area: Option, #[clap( long, help = "Specify the number of bytes sled is allowed to use for it's cache" )] #[serde(skip_serializing_if = "Option::is_none")] sled_cache_capacity: Option, #[clap( long, help = "Specify the number of events the console subscriber is allowed to buffer" )] #[serde(skip_serializing_if = "Option::is_none")] console_buffer_capacity: Option, #[clap( long, help = "An optional string to be checked on requests to privileged endpoints" )] #[serde(skip_serializing_if = "Option::is_none")] api_key: Option, #[clap( short, long, help = "Enable OpenTelemetry Tracing exports to the given OpenTelemetry collector" )] #[serde(skip_serializing_if = "Option::is_none")] opentelemetry_url: Option, #[serde(skip_serializing_if = "Option::is_none")] repo: Option, #[clap(flatten)] sled_repo: SledRepo, #[serde(skip_serializing_if = "Option::is_none")] store: Option, #[clap(flatten)] filesystem_storage: FilesystemStorage, #[clap(flatten)] object_storage: ObjectStorage, } impl ObjectStorage { pub(crate) fn required(&self) -> Result { Ok(RequiredObjectStorage { bucket_name: self .s3_store_bucket_name .as_ref() .cloned() .ok_or(RequiredError)?, region: self .s3_store_region .as_ref() .cloned() .map(Serde::into_inner) .ok_or(RequiredError)?, access_key: self.s3_store_access_key.as_ref().cloned(), security_token: self.s3_store_security_token.as_ref().cloned(), session_token: self.s3_store_session_token.as_ref().cloned(), }) } } 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.sled_cache_capacity.is_none() && self.console_buffer_capacity.is_none() && self.api_key.is_none() && self.opentelemetry_url.is_none() && self.repo.is_none() && self.store.is_none() } } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Subcommand)] #[serde(rename_all = "snake_case")] #[serde(tag = "type")] pub(crate) enum Command { Run, MigrateStore { to: Store }, MigrateRepo { to: Repo }, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ArgEnum)] #[serde(rename_all = "snake_case")] pub(crate) enum Repo { Sled, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct SledRepo { // defaults to {config.path} #[clap(long, help = "Path in which pict-rs will create it's 'repo' directory")] #[serde(skip_serializing_if = "Option::is_none")] sled_repo_path: Option, #[clap( long, help = "The number of bytes sled is allowed to use for it's in-memory cache" )] #[serde(skip_serializing_if = "Option::is_none")] sled_repo_cache_capacity: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ArgEnum)] #[serde(rename_all = "snake_case")] pub(crate) enum Store { Filesystem, #[cfg(feature = "object-storage")] ObjectStorage, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct FilesystemStorage { // defaults to {config.path} #[clap( long, help = "Path in which pict-rs will create it's 'files' directory" )] #[serde(skip_serializing_if = "Option::is_none")] filesystem_storage_path: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, Parser)] #[serde(rename_all = "snake_case")] pub(crate) struct ObjectStorage { #[serde(skip_serializing_if = "Option::is_none")] #[clap(long, help = "Name of the bucket in which pict-rs will store images")] s3_store_bucket_name: Option, #[serde(skip_serializing_if = "Option::is_none")] #[clap( long, help = "Region in which the bucket exists, can be an http endpoint" )] s3_store_region: Option>, #[serde(skip_serializing_if = "Option::is_none")] #[clap(long)] s3_store_access_key: Option, #[clap(long)] #[serde(skip_serializing_if = "Option::is_none")] s3_store_secret_key: Option, #[clap(long)] #[serde(skip_serializing_if = "Option::is_none")] s3_store_security_token: Option, #[clap(long)] #[serde(skip_serializing_if = "Option::is_none")] s3_store_session_token: Option, } pub(crate) struct RequiredObjectStorage { pub(crate) bucket_name: String, pub(crate) region: s3::Region, pub(crate) access_key: Option, pub(crate) security_token: Option, pub(crate) 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, sled_cache_capacity: u64, console_buffer_capacity: Option, api_key: Option, opentelemetry_url: Option, repo: Repo, sled_repo: SledRepo, store: Store, filesystem_storage: FilesystemStorage, object_storage: ObjectStorage, } #[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, sled_cache_capacity: u64, repo: Repo, sled_repo: SledRepoDefaults, store: Store, filesystem_store: FilesystemDefaults, } #[derive(serde::Serialize)] #[serde(rename_all = "snake_case")] struct SledRepoDefaults { sled_repo_cache_capacity: usize, } #[derive(serde::Serialize)] #[serde(rename_all = "snake_case")] struct FilesystemDefaults {} 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, sled_cache_capacity: 1024 * 1024 * 64, // 16 times smaller than sled's default of 1GB repo: Repo::Sled, sled_repo: SledRepoDefaults { sled_repo_cache_capacity: 1024 * 1024 * 64, }, store: Store::Filesystem, filesystem_store: FilesystemDefaults {}, } } } impl Config { pub(crate) fn build() -> anyhow::Result { let args = Args::parse(); let mut base_config = config::Config::builder().add_source(config::Config::try_from(&Defaults::new())?); if let Some(path) = args.config_file { base_config = base_config.add_source(config::File::from(path)); }; // TODO: Command parsing if !args.overrides.is_default() { let merging = config::Config::try_from(&args.overrides)?; base_config = base_config.add_source(merging); } let config: Self = base_config .add_source(config::Environment::with_prefix("PICTRS").separator("__")) .build()? .try_deserialize()?; Ok(config) } pub(crate) fn store(&self) -> &Store { &self.store } pub(crate) fn repo(&self) -> &Repo { &self.repo } pub(crate) fn object_storage(&self) -> Result { self.object_storage.required() } pub(crate) fn filesystem_storage_path(&self) -> Option<&PathBuf> { self.filesystem_storage.filesystem_storage_path.as_ref() } pub(crate) fn bind_address(&self) -> SocketAddr { self.addr } pub(crate) fn data_dir(&self) -> PathBuf { self.path.clone() } pub(crate) fn sled_cache_capacity(&self) -> u64 { self.sled_cache_capacity } pub(crate) fn console_buffer_capacity(&self) -> Option { self.console_buffer_capacity } 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(Debug, thiserror::Error)] #[error("Invalid store supplied, {0}")] pub(crate) struct StoreError(String); #[derive(Debug, thiserror::Error)] #[error("Invalid repo supplied, {0}")] pub(crate) struct RepoError(String); #[derive(Debug, thiserror::Error)] #[error("Missing required fields")] pub(crate) struct RequiredError; #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize, ArgEnum)] #[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 { for variant in Self::value_variants() { if variant.to_possible_value().unwrap().matches(s, false) { return Ok(*variant); } } Err(FormatError(s.into())) } } impl std::str::FromStr for Store { type Err = StoreError; 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(StoreError(s.into())) } } impl std::str::FromStr for Repo { type Err = RepoError; 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(RepoError(s.into())) } }