diff --git a/README.md b/README.md index 6dfd263..6805083 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ _a simple image hosting service_ ## Usage ### Running ``` -pict-rs 0.3.0-alpha.39 +pict-rs 0.3.0-alpha.42 USAGE: - pict-rs [FLAGS] [OPTIONS] --path + pict-rs [FLAGS] [OPTIONS] [SUBCOMMAND] FLAGS: -h, --help Prints help information @@ -20,33 +20,65 @@ FLAGS: -V, --version Prints version information OPTIONS: - -a, --addr - The address and port the server binds to. [env: PICTRS_ADDR=] [default: 0.0.0.0:8080] - - --api-key - An optional string to be checked on requests to privileged endpoints [env: PICTRS_API_KEY=] - + -a, --addr The address and port the server binds to. + --api-key An optional string to be checked on requests to privileged endpoints + -c, --config-file Path to the pict-rs configuration file -f, --filters ... - An optional list of filters to permit, supports 'identity', 'thumbnail', 'resize', 'crop', and 'blur' [env: - PICTRS_ALLOWED_FILTERS=] + An optional list of filters to permit, supports 'identity', 'thumbnail', 'resize', 'crop', and 'blur' + -i, --image-format - An optional image format to convert all uploaded files into, supports 'jpg', 'png', and 'webp' [env: - PICTRS_FORMAT=] - -m, --max-file-size - Specify the maximum allowed uploaded file size (in Megabytes) [env: PICTRS_MAX_FILE_SIZE=] [default: 40] - - --max-image-height - Specify the maximum width in pixels allowed on an image [env: PICTRS_MAX_IMAGE_HEIGHT=] [default: 10000] - - --max-image-width - Specify the maximum width in pixels allowed on an image [env: PICTRS_MAX_IMAGE_WIDTH=] [default: 10000] + An optional image format to convert all uploaded files into, supports 'jpg', 'png', and 'webp' + -m, --max-file-size Specify the maximum allowed uploaded file size (in Megabytes) + --max-image-area Specify the maximum area in pixels allowed in an image + --max-image-height Specify the maximum width in pixels allowed on an image + --max-image-width Specify the maximum width in pixels allowed on an image -o, --opentelemetry-url - Enable OpenTelemetry Tracing exports to the given OpenTelemetry collector [env: PICTRS_OPENTELEMETRY_URL=] + Enable OpenTelemetry Tracing exports to the given OpenTelemetry collector - -p, --path The path to the data directory, e.g. data/ [env: PICTRS_PATH=] + -p, --path The path to the data directory, e.g. data/ + +SUBCOMMANDS: + file-store + help Prints this message or the help of the given subcommand(s) + s3-store ``` +``` +pict-rs-file-store 0.3.0-alpha.42 + +USAGE: + pict-rs file-store [OPTIONS] + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + --path +``` + +``` +pict-rs-s3-store 0.3.0-alpha.42 + +USAGE: + pict-rs s3-store [OPTIONS] --bucket-name --region + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + --access-key + --bucket-name + --region + --secret-key + --security-token + --session-token +``` + +See [`pict-rs.toml`](https://git.asonix.dog/asonix/pict-rs/src/branch/main/pict-rs.toml) for more configuration + #### Example: Running on all interfaces, port 8080, storing data in /opt/data ``` @@ -60,6 +92,10 @@ Running locally, port 8080, storing data in data/, and only allowing the `thumbn ``` $ ./pict-rs -a 127.0.0.1:8080 -p data/ -w thumbnail identity ``` +Running from a configuration file +``` +$ ./pict-rs -c ./pict-rs.toml +``` #### Docker Run the following commands: diff --git a/pict-rs.toml b/pict-rs.toml index cf129b2..01ac91e 100644 --- a/pict-rs.toml +++ b/pict-rs.toml @@ -1,65 +1,116 @@ ## Required: path to store pict-rs database +# environment variable: PICTRS_PATH path = './data' ## Optional: pict-rs binding address +# environment variable: PICTRS_ADDR # default: 0.0.0.0:8080 addr = '0.0.0.0:8080' ## Optional: format to transcode all uploaded images +# environment variable: PICTRS_IMAGE_FORMAT # valid options: 'jpeg', 'png', 'webp' # default: empty # -# Not specifying image-format means images will be stored in their original format +# Not specifying image_format means images will be stored in their original format # This does not affect gif or mp4 uploads -image-format = 'jpeg' +image_format = 'jpeg' ## Optional: permitted image processing filters +# environment variable: PICTRS_FILTERS # valid options: 'identity', 'thumbnail', 'resize', 'crop', 'blur' # default: empty # # Not specifying filters implies all filters are permitted filters = [ - 'identity', - 'thumbnail', - 'resize', - 'crop', - 'blur', + 'identity', + 'thumbnail', + 'resize', + 'crop', + 'blur', ] ## Optional: image bounds +# environment variable: PICTRS_MAX_FILE_SIZE # default: 40 -max-file-size = 40 # in Megabytes +max_file_size = 40 # in Megabytes +# environment variable: PICTRS_MAX_IMAGE_WIDTH # default: 10,000 -max-image-width = 10000 # in Pixels +max_image_width = 10000 # in Pixels +# environment variable: PICTRS_MAX_IMAGE_HEIGHT # default: 10,000 -max-image-height = 10000 # in Pixels +max_image_height = 10000 # in Pixels +# environment variable: PICTRS_MAX_IMAGE_AREA # default: 40,000 -max-image-area = 40000 # in Pixels +max_image_area = 40000 # in Pixels -## Optional: +## Optional: skip image validation on the import endpoint +# environment variable: PICTRS_SKIP_VALIDATE_IMPORTS # default: false -skip-validate-imports = false +skip_validate_imports = false -## Optional: url for exporting otlp traces +## Optional: shared secret for internal endpoints +# environment variable: PICTRS_API_KEY # default: empty # -# Not specifying opentelemetry-url means no traces will be exported -opentelemetry-url = 'http://localhost:4317/' +# Not specifying api_key disables internal endpoints +api_key = 'API_KEY' + +## Optional: url for exporting otlp traces +# environment variable: PICTRS_OPENTELEMETRY_URL +# default: empty +# +# Not specifying opentelemetry_url means no traces will be exported +opentelemetry_url = 'http://localhost:4317/' ## Optional: store definition -# default: empty -# -# Not specifying a store will default to using a file-store in the same directory provided by the `path` field defined above +# default store: file_store [store] -type = 'file-store' -path = './data' +type = "file_store" + +## Example file store +# [store] +# +# # environment variable: PICTRS_STORE__TYPE +# type = 'file_store' +# +# # Optional: file path +# # environment variable: PICTRS_STORE__PATH +# # default: empty +# # +# # Not specifying path means pict-rs' top-level `path` config is used +# path = './data' ## Example s3 store # [store] -# type = 's3-store' -# bucket-name = 'rust-s3' +# +# # environment variable: PICTRS_STORE__TYPE +# type = 's3_store' +# +# # Required: bucket name +# # environment variable: PICTRS_STORE__BUCKET_NAME +# bucket_name = 'rust_s3' +# +# # Required: bucket region +# # environment variable: PICTRS_STORE__REGION # region = 'eu-central-1' # can also be endpoint of local s3 store -# access-key = 'ACCESS_KEY' -# secret-key = 'SECRET_KEY' -# security-token = 'SECURITY_TOKEN' -# session-token = 'SESSION-TOKEN' +# +# # Optional: bucket access key +# # environment variable: PICTRS_STORE__ACCESS_KEY +# # default: empty +# access_key = 'ACCESS_KEY' +# +# # Optional: bucket secret key +# # environment variable: PICTRS_STORE__SECRET_KEY +# # default: empty +# secret_key = 'SECRET_KEY' +# +# # Optional: bucket security token +# # environment variable: PICTRS_STORE__SECURITY_TOKEN +# # default: empty +# security_token = 'SECURITY_TOKEN' +# +# # Optional: bucket session token +# # environment variable: PICTRS_STORE__SESSION_TOKEN +# # default: empty +# session_token = 'SESSION_TOKEN' diff --git a/src/config.rs b/src/config.rs index 040155b..5d8a32d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,20 +13,27 @@ pub(crate) struct Args { overrides: Overrides, } +fn is_false(b: &bool) -> bool { + !b +} + #[derive(Clone, Debug, serde::Serialize, structopt::StructOpt)] -#[serde(rename_all = "kebab-case")] +#[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( @@ -34,6 +41,7 @@ pub(crate) struct Overrides { 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( @@ -41,6 +49,7 @@ pub(crate) struct Overrides { 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( @@ -48,21 +57,26 @@ pub(crate) struct Overrides { 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( @@ -70,33 +84,69 @@ pub(crate) struct Overrides { 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, structopt::StructOpt)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "snake_case")] #[serde(tag = "type")] pub(crate) enum Store { FileStore { // defaults to {config.path} + #[structopt(long)] + #[serde(skip_serializing_if = "Option::is_none")] path: Option, }, #[cfg(feature = "object-storage")] S3Store { + #[structopt(long)] bucket_name: String, + + #[structopt(long)] 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 = "kebab-case")] +#[serde(rename_all = "snake_case")] pub(crate) struct Config { skip_validate_imports: bool, addr: SocketAddr, @@ -113,7 +163,7 @@ pub(crate) struct Config { } #[derive(serde::Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "snake_case")] pub(crate) struct Defaults { skip_validate_imports: bool, addr: SocketAddr, @@ -141,17 +191,25 @@ impl Defaults { impl Config { pub(crate) fn build() -> anyhow::Result { let args = Args::from_args(); - let mut base_config = config::Config::try_from(&Defaults::new())?; + let mut base_config = config::Config::new(); + base_config.merge(config::Config::try_from(&Defaults::new())?)?; if let Some(path) = args.config_file { base_config.merge(config::File::from(path))?; }; - base_config.merge(config::Config::try_from(&args.overrides)?)?; + if !args.overrides.is_default() { + let merging = config::Config::try_from(&args.overrides)?; - base_config.merge(config::Environment::with_prefix("PICTRS"))?; + base_config.merge(merging)?; + } - Ok(base_config.try_into()?) + base_config.merge(config::Environment::with_prefix("PICTRS").separator("__"))?; + + let config: Self = base_config.try_into()?; + println!("{:#?}", config); + + Ok(config) } pub(crate) fn store(&self) -> &Store { @@ -208,7 +266,7 @@ impl Config { pub(crate) struct FormatError(String); #[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "snake_case")] pub(crate) enum Format { Jpeg, Png, diff --git a/src/middleware.rs b/src/middleware.rs index 3c08eff..356861c 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -220,10 +220,12 @@ where fn call(&self, req: ServiceRequest) -> Self::Future { if let Some(value) = req.headers().get("x-api-token") { - if value.to_str().is_ok() && value.to_str().ok() == self.0.as_deref() { - return InternalFuture::Internal { - future: self.1.call(req), - }; + if let (Ok(header), Some(api_key)) = (value.to_str(), &self.0) { + if header == api_key { + return InternalFuture::Internal { + future: self.1.call(req), + }; + } } }