feat: add CLI configuration flags
Add clap-powered --bind, --static-dir, and --data-dir flags for human-run server configuration. The merge order is now explicit: command-line arguments win over UPL_* environment variables, which still fall back to the existing repository-local defaults. Document the new flags and allow just run to forward arguments to cargo so the help text can be checked through the normal task runner. Test Plan: - just check - cargo run -- --help Refs: none
This commit is contained in:
Generated
+121
@@ -2,6 +2,56 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
@@ -90,6 +140,52 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -314,6 +410,12 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -416,6 +518,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -635,6 +743,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -833,6 +947,7 @@ name = "upl"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"clap",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -844,6 +959,12 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.23.2"
|
version = "1.23.2"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8.9"
|
axum = "0.8.9"
|
||||||
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
time = { version = "0.3.47", features = ["formatting", "serde"] }
|
time = { version = "0.3.47", features = ["formatting", "serde"] }
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ upl
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- `UPL_BIND` sets the listen address. It defaults to `127.0.0.1:3000`.
|
- `--bind` sets the listen address. It overrides `UPL_BIND` and defaults to
|
||||||
- `UPL_STATIC_DIR` sets the static asset directory. It defaults to `static/`
|
`127.0.0.1:3000`.
|
||||||
inside this repository.
|
- `--static-dir` sets the static asset directory. It overrides `UPL_STATIC_DIR`
|
||||||
- `UPL_DATA_DIR` sets the upload data directory. It defaults to `data/` inside
|
and defaults to `static/` inside this repository.
|
||||||
this repository.
|
- `--data-dir` sets the upload data directory. It overrides `UPL_DATA_DIR` and
|
||||||
|
defaults to `data/` inside this repository.
|
||||||
|
- `upl --help` prints the full argument help text.
|
||||||
- The server accepts request bodies up to 64 MiB, which leaves room for the
|
- The server accepts request bodies up to 64 MiB, which leaves room for the
|
||||||
planned 16 MiB upload chunks and matches the nginx example in `PLAN.md`.
|
planned 16 MiB upload chunks and matches the nginx example in `PLAN.md`.
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ check:
|
|||||||
just static-check
|
just static-check
|
||||||
just clippy
|
just clippy
|
||||||
|
|
||||||
run:
|
run *args:
|
||||||
cargo run
|
cargo run -- {{args}}
|
||||||
|
|||||||
+134
-6
@@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
ffi::OsString,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
@@ -10,6 +11,7 @@ use axum::{
|
|||||||
extract::DefaultBodyLimit,
|
extract::DefaultBodyLimit,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
use clap::Parser;
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
use crate::{api, storage::Storage};
|
use crate::{api, storage::Storage};
|
||||||
@@ -32,18 +34,79 @@ pub struct AppState {
|
|||||||
pub storage: Storage,
|
pub storage: Storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "upl",
|
||||||
|
version,
|
||||||
|
about = "Run the upl resumable upload server.",
|
||||||
|
long_about = "Run the upl resumable upload server.\n\nCommand-line arguments override environment variables. When neither is set, upl uses local development defaults inside the repository.",
|
||||||
|
after_help = "Environment variables:\n UPL_BIND Default listen address\n UPL_STATIC_DIR Default static asset directory\n UPL_DATA_DIR Default upload data directory"
|
||||||
|
)]
|
||||||
|
pub struct CliArgs {
|
||||||
|
/// Socket address to listen on. Overrides `UPL_BIND`. Defaults to 127.0.0.1:3000.
|
||||||
|
#[arg(long, value_name = "ADDR")]
|
||||||
|
pub bind: Option<SocketAddr>,
|
||||||
|
|
||||||
|
/// Directory containing index.html and other browser assets. Overrides `UPL_STATIC_DIR`.
|
||||||
|
#[arg(long, value_name = "PATH")]
|
||||||
|
pub static_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Directory where upload staging chunks and completed files are written. Overrides `UPL_DATA_DIR`.
|
||||||
|
#[arg(long, value_name = "PATH")]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
/// Loads bind and static directory settings from environment variables.
|
/// Loads settings from command-line arguments and environment variables.
|
||||||
|
///
|
||||||
|
/// Command-line arguments take precedence over environment variables.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error when an argument or `UPL_BIND` is not a valid socket
|
||||||
|
/// address.
|
||||||
|
pub fn from_args() -> Result<Self, Box<dyn Error>> {
|
||||||
|
Self::from_cli_and_env(CliArgs::parse())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads settings from environment variables.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error when `UPL_BIND` is set but is not a valid socket address.
|
/// Returns an error when `UPL_BIND` is set but is not a valid socket address.
|
||||||
pub fn from_env() -> Result<Self, Box<dyn Error>> {
|
pub fn from_env() -> Result<Self, Box<dyn Error>> {
|
||||||
let bind_addr = env::var(BIND_ENV)
|
Self::from_cli_and_env(CliArgs::default())
|
||||||
.unwrap_or_else(|_| DEFAULT_BIND_ADDR.to_owned())
|
}
|
||||||
.parse()?;
|
|
||||||
let static_dir = env::var_os(STATIC_DIR_ENV).map_or_else(default_static_dir, PathBuf::from);
|
fn from_cli_and_env(cli: CliArgs) -> Result<Self, Box<dyn Error>> {
|
||||||
let data_dir = env::var_os(DATA_DIR_ENV).map_or_else(default_data_dir, PathBuf::from);
|
Self::from_sources(
|
||||||
|
cli,
|
||||||
|
env::var(BIND_ENV).ok(),
|
||||||
|
env::var_os(STATIC_DIR_ENV),
|
||||||
|
env::var_os(DATA_DIR_ENV),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_sources(
|
||||||
|
cli: CliArgs,
|
||||||
|
bind_env: Option<String>,
|
||||||
|
static_dir_env: Option<OsString>,
|
||||||
|
data_dir_env: Option<OsString>,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let bind_addr = match (cli.bind, bind_env) {
|
||||||
|
(Some(bind_addr), _) => bind_addr,
|
||||||
|
(None, Some(bind_addr)) => bind_addr.parse()?,
|
||||||
|
(None, None) => DEFAULT_BIND_ADDR.parse()?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let static_dir = cli
|
||||||
|
.static_dir
|
||||||
|
.or_else(|| static_dir_env.map(PathBuf::from))
|
||||||
|
.unwrap_or_else(default_static_dir);
|
||||||
|
let data_dir = cli
|
||||||
|
.data_dir
|
||||||
|
.or_else(|| data_dir_env.map(PathBuf::from))
|
||||||
|
.unwrap_or_else(default_data_dir);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
bind_addr,
|
bind_addr,
|
||||||
@@ -103,3 +166,68 @@ fn default_static_dir() -> PathBuf {
|
|||||||
fn default_data_dir() -> PathBuf {
|
fn default_data_dir() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data")
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{ffi::OsString, net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use super::{AppConfig, CliArgs};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_config_arguments() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = CliArgs::try_parse_from([
|
||||||
|
"upl",
|
||||||
|
"--bind",
|
||||||
|
"127.0.0.1:4000",
|
||||||
|
"--static-dir",
|
||||||
|
"public",
|
||||||
|
"--data-dir",
|
||||||
|
"uploads",
|
||||||
|
])?;
|
||||||
|
|
||||||
|
assert_eq!(args.bind, Some("127.0.0.1:4000".parse()?));
|
||||||
|
assert_eq!(args.static_dir, Some(PathBuf::from("public")));
|
||||||
|
assert_eq!(args.data_dir, Some(PathBuf::from("uploads")));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_arguments_override_environment_values() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = AppConfig::from_sources(
|
||||||
|
CliArgs {
|
||||||
|
bind: Some("127.0.0.1:4000".parse()?),
|
||||||
|
static_dir: Some(PathBuf::from("cli-static")),
|
||||||
|
data_dir: Some(PathBuf::from("cli-data")),
|
||||||
|
},
|
||||||
|
Some("127.0.0.1:3001".to_owned()),
|
||||||
|
Some(OsString::from("env-static")),
|
||||||
|
Some(OsString::from("env-data")),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(config.bind_addr, "127.0.0.1:4000".parse::<SocketAddr>()?);
|
||||||
|
assert_eq!(config.static_dir, PathBuf::from("cli-static"));
|
||||||
|
assert_eq!(config.data_dir, PathBuf::from("cli-data"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn environment_values_are_used_when_arguments_are_absent()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = AppConfig::from_sources(
|
||||||
|
CliArgs::default(),
|
||||||
|
Some("127.0.0.1:3001".to_owned()),
|
||||||
|
Some(OsString::from("env-static")),
|
||||||
|
Some(OsString::from("env-data")),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(config.bind_addr, "127.0.0.1:3001".parse::<SocketAddr>()?);
|
||||||
|
assert_eq!(config.static_dir, PathBuf::from("env-static"));
|
||||||
|
assert_eq!(config.data_dir, PathBuf::from("env-data"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ use upl::app::{AppConfig, build_router};
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config = AppConfig::from_env()?;
|
let config = AppConfig::from_args()?;
|
||||||
let listener = tokio::net::TcpListener::bind(config.bind_addr).await?;
|
let listener = tokio::net::TcpListener::bind(config.bind_addr).await?;
|
||||||
|
|
||||||
println!("upl listening on http://{}", listener.local_addr()?);
|
println!("upl listening on http://{}", listener.local_addr()?);
|
||||||
|
|||||||
Reference in New Issue
Block a user