diff --git a/src/config.rs b/src/config.rs index c7cb2be..737a61c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,592 +1,12 @@ -use crate::serde_str::Serde; -use clap::{ArgEnum, Parser, Subcommand}; -use std::{collections::HashSet, net::SocketAddr, path::PathBuf}; -use url::Url; +mod commandline; +mod defaults; +mod file; +mod primitives; 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, +pub(crate) use file::ConfigFile as Configuration; - #[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 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")] - #[clap( - short = 'R', - long, - help = "Set the database implementation. Available options are 'sled'. Default is 'sled'" - )] - repo: Option, - - #[clap(flatten)] - sled: Sled, - - #[serde(skip_serializing_if = "Option::is_none")] - #[clap( - short = 'S', - long, - help = "Set the image store. Available options are 'object-storage' or 'filesystem'. Default is 'filesystem'" - )] - 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 - .object_store_bucket_name - .as_ref() - .cloned() - .ok_or(RequiredError("object-store-bucket-name"))?, - region: self - .object_store_region - .as_ref() - .cloned() - .map(Serde::into_inner) - .ok_or(RequiredError("object-store-region"))?, - access_key: self.object_store_access_key.as_ref().cloned(), - secret_key: self.object_store_secret_key.as_ref().cloned(), - security_token: self.object_store_security_token.as_ref().cloned(), - session_token: self.object_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.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, - Dump { path: PathBuf }, - MigrateStore { to: Store }, - MigrateRepo { to: Repo }, -} - -pub(crate) enum CommandConfig { - Run, - Dump { - path: PathBuf, - }, - MigrateStore { - to: Storage, - }, - MigrateRepo { - #[allow(dead_code)] - to: Repository, - }, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, ArgEnum)] -#[serde(rename_all = "snake_case")] -pub(crate) enum Repo { - Sled, -} - -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, Parser)] -#[serde(rename_all = "snake_case")] -pub(crate) struct Sled { - // 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")] - pub(crate) sled_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")] - pub(crate) sled_cache_capacity: Option, -} - -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, ArgEnum)] -#[serde(rename_all = "snake_case")] -pub(crate) enum Store { - Filesystem, - ObjectStorage, -} - -#[derive(Clone, Debug, Default, 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")] - pub(crate) filesystem_storage_path: Option, -} - -#[derive(Clone, Debug, Default, 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")] - object_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" - )] - object_store_region: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - #[clap(long)] - object_store_access_key: Option, - - #[clap(long)] - #[serde(skip_serializing_if = "Option::is_none")] - object_store_secret_key: Option, - - #[clap(long)] - #[serde(skip_serializing_if = "Option::is_none")] - object_store_security_token: Option, - - #[clap(long)] - #[serde(skip_serializing_if = "Option::is_none")] - object_store_session_token: Option, -} - -pub(crate) struct RequiredSledRepo { - pub(crate) path: PathBuf, - pub(crate) cache_capacity: u64, -} - -pub(crate) struct RequiredObjectStorage { - pub(crate) bucket_name: String, - pub(crate) region: s3::Region, - pub(crate) access_key: Option, - pub(crate) secret_key: Option, - pub(crate) security_token: Option, - pub(crate) session_token: Option, -} - -pub(crate) struct RequiredFilesystemStorage { - pub(crate) path: PathBuf, -} - -pub(crate) enum Storage { - ObjectStorage(RequiredObjectStorage), - Filesystem(RequiredFilesystemStorage), -} - -pub(crate) enum Repository { - Sled(RequiredSledRepo), -} - -#[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, - console_buffer_capacity: Option, - api_key: Option, - opentelemetry_url: Option, - repo: Repo, - store: Store, - command: Command, - sled: Option, - filesystem_storage: Option, - object_storage: Option, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "snake_case")] -pub(crate) struct Defaults { - command: Command, - skip_validate_imports: bool, - addr: SocketAddr, - max_file_size: usize, - max_image_width: usize, - max_image_height: usize, - max_image_area: usize, - repo: Repo, - sled: SledDefaults, - store: Store, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "snake_case")] -struct SledDefaults { - sled_cache_capacity: usize, -} - -impl Defaults { - fn new(command: Command) -> Self { - Defaults { - command, - 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, - repo: Repo::Sled, - sled: SledDefaults { - sled_cache_capacity: 1024 * 1024 * 64, - }, - store: Store::Filesystem, - } - } -} - -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(args.command))?); - - if let Some(path) = args.config_file { - base_config = base_config.add_source(config::File::from(path)); - }; - - 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 command(&self) -> anyhow::Result { - Ok(match &self.command { - Command::Run => CommandConfig::Run, - Command::Dump { path } => CommandConfig::Dump { path: path.clone() }, - Command::MigrateStore { to } => CommandConfig::MigrateStore { - to: match to { - Store::ObjectStorage => Storage::ObjectStorage( - self.object_storage - .as_ref() - .cloned() - .unwrap_or_default() - .required()?, - ), - Store::Filesystem => Storage::Filesystem(RequiredFilesystemStorage { - path: self - .filesystem_storage - .as_ref() - .and_then(|f| f.filesystem_storage_path.clone()) - .unwrap_or_else(|| { - let mut path = self.path.clone(); - path.push("files"); - path - }), - }), - }, - }, - Command::MigrateRepo { to } => CommandConfig::MigrateRepo { - to: match to { - Repo::Sled => { - let sled = self.sled.as_ref().cloned().unwrap_or_default(); - - Repository::Sled(RequiredSledRepo { - path: sled.sled_path.unwrap_or_else(|| { - let mut path = self.path.clone(); - path.push("sled-repo"); - path - }), - cache_capacity: sled.sled_cache_capacity.unwrap_or(1024 * 1024 * 64), - }) - } - }, - }, - }) - } - - pub(crate) fn store(&self) -> anyhow::Result { - Ok(match self.store { - Store::Filesystem => Storage::Filesystem(RequiredFilesystemStorage { - path: self - .filesystem_storage - .as_ref() - .and_then(|f| f.filesystem_storage_path.clone()) - .unwrap_or_else(|| { - let mut path = self.path.clone(); - path.push("files"); - path - }), - }), - Store::ObjectStorage => Storage::ObjectStorage( - self.object_storage - .as_ref() - .cloned() - .unwrap_or_default() - .required()?, - ), - }) - } - - pub(crate) fn repo(&self) -> Repository { - match self.repo { - Repo::Sled => { - let sled = self.sled.as_ref().cloned().unwrap_or_default(); - - Repository::Sled(RequiredSledRepo { - path: sled.sled_path.unwrap_or_else(|| { - let mut path = self.path.clone(); - path.push("sled-repo"); - path - }), - cache_capacity: sled.sled_cache_capacity.unwrap_or(1024 * 1024 * 64), - }) - } - } - } - - pub(crate) fn bind_address(&self) -> SocketAddr { - self.addr - } - - pub(crate) fn data_dir(&self) -> PathBuf { - self.path.clone() - } - - 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 {0} field")] -pub(crate) struct RequiredError(&'static str); - -#[derive(Clone, Copy, 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())) - } +pub(crate) fn configure() -> anyhow::Result { + unimplemented!() } diff --git a/src/config/commandline.rs b/src/config/commandline.rs new file mode 100644 index 0000000..89dd726 --- /dev/null +++ b/src/config/commandline.rs @@ -0,0 +1,216 @@ +use crate::config::primitives::{ImageFormat, LogFormat, Targets}; +use clap::{Parser, Subcommand}; +use std::{net::SocketAddr, path::PathBuf}; +use url::Url; + +/// Run the pict-rs application +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +pub(crate) struct Args { + /// Path to the pict-rs configuration file + #[clap(short, long)] + pub(crate) config_file: Option, + + /// Format of logs printed to stdout + #[clap(long)] + pub(crate) log_format: Option, + /// Log levels to print to stdout, respects RUST_LOG formatting + #[clap(long)] + pub(crate) log_targets: Option, + + /// Address and port to expose tokio-console metrics + #[clap(long)] + pub(crate) console_address: Option, + /// Capacity of the console-subscriber Event Buffer + #[clap(long)] + pub(crate) console_buffer_capacity: Option, + + /// URL to send OpenTelemetry metrics + #[clap(long)] + pub(crate) opentelemetry_url: Option, + /// Service Name to use for OpenTelemetry + #[clap(long)] + pub(crate) opentelemetry_service_name: Option, + /// Log levels to use for OpenTelemetry, respects RUST_LOG formatting + #[clap(long)] + pub(crate) opentelemetry_targets: Option, + + /// File to save the current configuration for reproducible runs + #[clap(long)] + pub(crate) save_to: Option, + + #[clap(subcommand)] + pub(crate) command: Command, +} + +#[derive(Debug, Subcommand)] +pub(crate) enum Command { + /// Runs the pict-rs web server + Run(Run), + + /// Migrates from one provided media store to another + #[clap(flatten)] + MigrateStore(MigrateStore), +} + +#[derive(Debug, Parser)] +pub(crate) struct Run { + /// The address and port to bind the pict-rs web server + #[clap(short, long)] + pub(crate) address: SocketAddr, + + /// The API KEY required to access restricted routes + #[clap(long)] + pub(crate) api_key: Option, + + /// Whether to validate media on the "import" endpoint + #[clap(long)] + pub(crate) media_skip_validate_imports: Option, + /// The maximum width, in pixels, for uploaded media + #[clap(long)] + pub(crate) media_max_width: Option, + /// The maximum height, in pixels, for uploaded media + #[clap(long)] + pub(crate) media_max_height: Option, + /// The maximum area, in pixels, for uploaded media + #[clap(long)] + pub(crate) media_max_area: Option, + /// The maximum size, in megabytes, for uploaded media + #[clap(long)] + pub(crate) media_max_file_size: Option, + /// Whether to enable GIF and silent MP4 uploads. Full videos are unsupported + #[clap(long)] + pub(crate) media_enable_silent_video: Option, + /// Which media filters should be enabled on the `process` endpoint + #[clap(long)] + pub(crate) media_filters: Option>, + /// Enforce uploaded media is transcoded to the provided format + #[clap(long)] + pub(crate) media_format: Option, + + #[clap(subcommand)] + pub(crate) store: Option, +} + +/// Configure the provided storage +#[derive(Debug, Subcommand)] +pub(crate) enum Store { + /// configure filesystem storage + Filesystem(Filesystem), + + /// configure object storage + ObjectStorage(ObjectStorage), +} + +/// Run pict-rs with the provided storage +#[derive(Debug, Subcommand)] +pub(crate) enum RunStore { + /// Run pict-rs with filesystem storage + Filesystem(RunFilesystem), + + /// Run pict-rs with object storage + ObjectStorage(RunObjectStorage), +} + +/// Configure the pict-rs storage migration +#[derive(Debug, Subcommand)] +pub(crate) enum MigrateStore { + /// Migrate from the provided filesystem storage + Filesystem(MigrateFilesystem), + + /// Migrate from the provided object storage + ObjectStorage(MigrateObjectStorage), +} + +/// Migrate pict-rs' storage from the provided filesystem storage +#[derive(Debug, Parser)] +pub(crate) struct MigrateFilesystem { + #[clap(flatten)] + pub(crate) from: Filesystem, + + #[clap(subcommand)] + pub(crate) to: RunStore, +} + +/// Migrate pict-rs' storage from the provided object storage +#[derive(Debug, Parser)] +pub(crate) struct MigrateObjectStorage { + #[clap(flatten)] + pub(crate) from: ObjectStorage, + + #[clap(subcommand)] + pub(crate) to: RunStore, +} + +/// Run pict-rs with the provided filesystem storage +#[derive(Debug, Parser)] +pub(crate) struct RunFilesystem { + #[clap(flatten)] + pub(crate) system: Filesystem, + + #[clap(subcommand)] + pub(crate) repo: Repo, +} + +/// Run pict-rs with the provided object storage +#[derive(Debug, Parser)] +pub(crate) struct RunObjectStorage { + #[clap(flatten)] + pub(crate) storage: ObjectStorage, + + #[clap(subcommand)] + pub(crate) repo: Repo, +} + +/// Configuration for data repositories +#[derive(Debug, Subcommand)] +pub(crate) enum Repo { + /// Run pict-rs with the provided sled-backed data repository + Sled(Sled), +} + +/// Configuration for filesystem media storage +#[derive(Debug, Parser)] +pub(crate) struct Filesystem { + /// The path to store uploaded media + #[clap(short, long)] + pub(crate) path: Option, +} + +/// Configuration for Object Storage +#[derive(Debug, Parser)] +pub(crate) struct ObjectStorage { + /// The bucket in which to store media + #[clap(short, long)] + pub(crate) bucket_name: Option, + + /// The region the bucket is located in + #[clap(short, long)] + pub(crate) region: Option, + + /// The Access Key for the user accessing the bucket + #[clap(short, long)] + pub(crate) access_key: Option, + + /// The secret key for the user accessing the bucket + #[clap(short, long)] + pub(crate) secret_key: Option, + + /// The security token for accessing the bucket + #[clap(long)] + pub(crate) security_token: Option, + + /// The session token for accessing the bucket + #[clap(long)] + pub(crate) session_token: Option, +} + +/// Configuration for the sled-backed data repository +#[derive(Debug, Parser)] +pub(crate) struct Sled { + /// The path to store the sled database + pub(crate) path: Option, + + /// The cache capacity, in bytes, allowed to sled for in-memory operations + pub(crate) cache_capacity: Option, +} diff --git a/src/config/defaults.rs b/src/config/defaults.rs new file mode 100644 index 0000000..04ad61f --- /dev/null +++ b/src/config/defaults.rs @@ -0,0 +1,181 @@ +use crate::{ + config::primitives::{LogFormat, Targets}, + serde_str::Serde, +}; +use std::{net::SocketAddr, path::PathBuf}; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Defaults { + server: ServerDefaults, + tracing: TracingDefaults, + old_db: OldDbDefaults, + media: MediaDefaults, + repo: RepoDefaults, + store: StoreDefaults, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct ServerDefaults { + address: SocketAddr, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct TracingDefaults { + logging: LoggingDefaults, + + console: ConsoleDefaults, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct LoggingDefaults { + format: LogFormat, + targets: Serde, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct ConsoleDefaults { + buffer_capacity: usize, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct OldDbDefaults { + path: PathBuf, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct MediaDefaults { + max_width: usize, + max_height: usize, + max_area: usize, + max_file_size: usize, + enable_silent_video: bool, + filters: Vec, + skip_validate_imports: bool, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +enum RepoDefaults { + Sled(SledDefaults), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct SledDefaults { + path: PathBuf, + cache_capacity: u64, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +enum StoreDefaults { + Filesystem(FilesystemDefaults), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct FilesystemDefaults { + path: PathBuf, +} + +impl Default for Defaults { + fn default() -> Self { + Defaults { + server: ServerDefaults::default(), + tracing: TracingDefaults::default(), + old_db: OldDbDefaults::default(), + media: MediaDefaults::default(), + repo: RepoDefaults::default(), + store: StoreDefaults::default(), + } + } +} + +impl Default for ServerDefaults { + fn default() -> Self { + ServerDefaults { + address: "0.0.0.0:8080".parse().expect("Valid address string"), + } + } +} + +impl Default for TracingDefaults { + fn default() -> TracingDefaults { + TracingDefaults { + logging: LoggingDefaults::default(), + console: ConsoleDefaults::default(), + } + } +} + +impl Default for LoggingDefaults { + fn default() -> Self { + LoggingDefaults { + format: LogFormat::Normal, + targets: "info".parse().expect("Valid targets string"), + } + } +} + +impl Default for ConsoleDefaults { + fn default() -> Self { + ConsoleDefaults { + buffer_capacity: 1024 * 100, + } + } +} + +impl Default for OldDbDefaults { + fn default() -> Self { + OldDbDefaults { + path: PathBuf::from(String::from("/mnt")), + } + } +} + +impl Default for MediaDefaults { + fn default() -> Self { + MediaDefaults { + max_width: 10_000, + max_height: 10_000, + max_area: 40_000_000, + max_file_size: 40, + enable_silent_video: true, + filters: vec![ + "identity".into(), + "thumbnail".into(), + "resize".into(), + "crop".into(), + "blur".into(), + ], + skip_validate_imports: false, + } + } +} + +impl Default for RepoDefaults { + fn default() -> Self { + Self::Sled(SledDefaults::default()) + } +} + +impl Default for SledDefaults { + fn default() -> Self { + SledDefaults { + path: PathBuf::from(String::from("/mnt/sled-repo")), + cache_capacity: 1024 * 1024 * 64, + } + } +} + +impl Default for StoreDefaults { + fn default() -> Self { + Self::Filesystem(FilesystemDefaults::default()) + } +} + +impl Default for FilesystemDefaults { + fn default() -> Self { + Self { + path: PathBuf::from(String::from("/mnt/files")), + } + } +} diff --git a/src/config/file.rs b/src/config/file.rs new file mode 100644 index 0000000..26e298b --- /dev/null +++ b/src/config/file.rs @@ -0,0 +1,131 @@ +use crate::{ + config::primitives::{ImageFormat, LogFormat, Targets}, + serde_str::Serde, +}; +use std::{net::SocketAddr, path::PathBuf}; +use url::Url; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct ConfigFile { + pub(crate) server: Server, + + pub(crate) tracing: Tracing, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) old_db: Option, + + pub(crate) media: Media, + + pub(crate) repo: Repo, + + pub(crate) store: Store, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub(crate) enum Repo { + Sled(Sled), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub(crate) enum Store { + Filesystem(Filesystem), + + ObjectStorage(ObjectStorage), +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Server { + pub(crate) address: SocketAddr, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) api_key: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Tracing { + logging: Logging, + + #[serde(skip_serializing_if = "Option::is_none")] + console: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + opentelemetry: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Logging { + pub(crate) format: LogFormat, + + pub(crate) targets: Serde, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct OpenTelemetry { + pub(crate) url: Url, + + pub(crate) service_name: String, + + pub(crate) targets: Serde, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Console { + pub(crate) address: SocketAddr, + pub(crate) buffer_capacity: usize, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct OldDb { + pub(crate) path: PathBuf, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Media { + pub(crate) max_width: usize, + + pub(crate) max_height: usize, + + pub(crate) max_area: usize, + + pub(crate) max_file_size: usize, + + pub(crate) enable_silent_video: bool, + + pub(crate) filters: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) format: Option, + + pub(crate) skip_validate_imports: bool, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Filesystem { + pub(crate) path: PathBuf, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct ObjectStorage { + pub(crate) bucket_name: String, + + pub(crate) region: Serde, + + pub(crate) access_key: String, + + pub(crate) secret_key: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) security_token: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) session_token: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Sled { + pub(crate) path: PathBuf, + + pub(crate) cache_capacity: u64, +} diff --git a/src/config/primitives.rs b/src/config/primitives.rs new file mode 100644 index 0000000..44d1ccd --- /dev/null +++ b/src/config/primitives.rs @@ -0,0 +1,111 @@ +use clap::ArgEnum; +use std::{fmt::Display, str::FromStr}; + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + serde::Deserialize, + serde::Serialize, + ArgEnum, +)] +pub(crate) enum LogFormat { + Compact, + Json, + Normal, + Pretty, +} + +#[derive( + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + serde::Deserialize, + serde::Serialize, + ArgEnum, +)] +pub(crate) enum ImageFormat { + Jpeg, + Webp, + Png, +} + +#[derive(Clone, Debug)] +pub(crate) struct Targets { + pub(crate) targets: tracing_subscriber::filter::Targets, +} + +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(","); + + write!(f, "{}", targets) + } +} + +impl FromStr for ImageFormat { + 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 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 ImageFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.to_possible_value() + .expect("no values are skipped") + .get_name() + .fmt(f) + } +} + +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) + } +}