feat: more flexible config values (#299)

This commit is contained in:
sigoden 2023-11-27 04:24:25 +08:00 committed by GitHub
parent 7584fe3d08
commit 6ff8b29b69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 26 deletions

12
Cargo.lock generated
View file

@ -492,6 +492,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha-crypt", "sha-crypt",
"smart-default",
"socket2 0.5.5", "socket2 0.5.5",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -1552,6 +1553,17 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "smart-default"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.4.10" version = "0.4.10"

View file

@ -47,6 +47,7 @@ indexmap = "2.0"
serde_yaml = "0.9.27" serde_yaml = "0.9.27"
sha-crypt = "0.5.0" sha-crypt = "0.5.0"
base64 = "0.21.5" base64 = "0.21.5"
smart-default = "0.7.1"
[features] [features]
default = ["tls"] default = ["tls"]

View file

@ -235,8 +235,7 @@ $6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8
Use hashed password Use hashed password
``` ```
dufs \ dufs -a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw'
-a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw'
``` ```
Two important things for hashed passwords: Two important things for hashed passwords:
@ -256,9 +255,10 @@ dufs --hidden .git,.DS_Store,tmp
> The glob used in --hidden only matches file and directory names, not paths. So `--hidden dir1/file` is invalid. > The glob used in --hidden only matches file and directory names, not paths. So `--hidden dir1/file` is invalid.
```sh ```sh
dufs --hidden '.*' # hidden dotfiles dufs --hidden '.*' # hidden dotfiles
dufs --hidden '*/' # hidden all folders dufs --hidden '*/' # hidden all folders
dufs --hidden '*.log,*.lock' # hidden by exts dufs --hidden '*.log,*.lock' # hidden by exts
dufs --hidden '*.log' --hidden '*.lock'
``` ```
### Log Format ### Log Format
@ -311,8 +311,8 @@ All options can be set using environment variables prefixed with `DUFS_`.
--config <path> DUFS_CONFIG=config.yaml --config <path> DUFS_CONFIG=config.yaml
-b, --bind <addrs> DUFS_BIND=0.0.0.0 -b, --bind <addrs> DUFS_BIND=0.0.0.0
-p, --port <port> DUFS_PORT=5000 -p, --port <port> DUFS_PORT=5000
--path-prefix <path> DUFS_PATH_PREFIX=/path --path-prefix <path> DUFS_PATH_PREFIX=/static
--hidden <value> DUFS_HIDDEN=*.log --hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/" -a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
-A, --allow-all DUFS_ALLOW_ALL=true -A, --allow-all DUFS_ALLOW_ALL=true
--allow-upload DUFS_ALLOW_UPLOAD=true --allow-upload DUFS_ALLOW_UPLOAD=true
@ -338,8 +338,7 @@ The following are the configuration items:
```yaml ```yaml
serve-path: '.' serve-path: '.'
bind: bind: 0.0.0.0
- 192.168.8.10
port: 5000 port: 5000
path-prefix: /dufs path-prefix: /dufs
hidden: hidden:

View file

@ -3,6 +3,7 @@ use clap::builder::PossibleValuesParser;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use clap_complete::{generate, Generator, Shell}; use clap_complete::{generate, Generator, Shell};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use smart_default::SmartDefault;
use std::env; use std::env;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -70,6 +71,8 @@ pub fn build_cli() -> Command {
.env("DUFS_HIDDEN") .env("DUFS_HIDDEN")
.hide_env(true) .hide_env(true)
.long("hidden") .long("hidden")
.action(ArgAction::Append)
.value_delimiter(',')
.help("Hide paths from directory listings, e.g. tmp,*.log,*.lock") .help("Hide paths from directory listings, e.g. tmp,*.log,*.lock")
.value_name("value"), .value_name("value"),
) )
@ -228,23 +231,27 @@ pub fn print_completions<G: Generator>(gen: G, cmd: &mut Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
} }
#[derive(Debug, Deserialize, Default)] #[derive(Debug, Deserialize, SmartDefault, PartialEq)]
#[serde(default)] #[serde(default)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Args { pub struct Args {
#[serde(default = "default_serve_path")] #[serde(default = "default_serve_path")]
#[default(default_serve_path())]
pub serve_path: PathBuf, pub serve_path: PathBuf,
#[serde(deserialize_with = "deserialize_bind_addrs")] #[serde(deserialize_with = "deserialize_bind_addrs")]
#[serde(rename = "bind")] #[serde(rename = "bind")]
#[serde(default = "default_addrs")] #[serde(default = "default_addrs")]
#[default(default_addrs())]
pub addrs: Vec<BindAddr>, pub addrs: Vec<BindAddr>,
#[serde(default = "default_port")] #[serde(default = "default_port")]
#[default(default_port())]
pub port: u16, pub port: u16,
#[serde(skip)] #[serde(skip)]
pub path_is_file: bool, pub path_is_file: bool,
pub path_prefix: String, pub path_prefix: String,
#[serde(skip)] #[serde(skip)]
pub uri_prefix: String, pub uri_prefix: String,
#[serde(deserialize_with = "deserialize_string_or_vec")]
pub hidden: Vec<String>, pub hidden: Vec<String>,
#[serde(deserialize_with = "deserialize_access_control")] #[serde(deserialize_with = "deserialize_access_control")]
pub auth: AccessControl, pub auth: AccessControl,
@ -272,12 +279,7 @@ impl Args {
/// If a parsing error occurred, exit the process and print out informative /// If a parsing error occurred, exit the process and print out informative
/// error message to user. /// error message to user.
pub fn parse(matches: ArgMatches) -> Result<Args> { pub fn parse(matches: ArgMatches) -> Result<Args> {
let mut args = Self { let mut args = Self::default();
serve_path: default_serve_path(),
addrs: default_addrs(),
port: default_port(),
..Default::default()
};
if let Some(config_path) = matches.get_one::<PathBuf>("config") { if let Some(config_path) = matches.get_one::<PathBuf>("config") {
let contents = std::fs::read_to_string(config_path) let contents = std::fs::read_to_string(config_path)
@ -289,6 +291,7 @@ impl Args {
if let Some(path) = matches.get_one::<PathBuf>("serve-path") { if let Some(path) = matches.get_one::<PathBuf>("serve-path") {
args.serve_path = path.clone() args.serve_path = path.clone()
} }
args.serve_path = Self::sanitize_path(args.serve_path)?; args.serve_path = Self::sanitize_path(args.serve_path)?;
if let Some(port) = matches.get_one::<u16>("port") { if let Some(port) = matches.get_one::<u16>("port") {
@ -312,11 +315,15 @@ impl Args {
format!("/{}/", &encode_uri(&args.path_prefix)) format!("/{}/", &encode_uri(&args.path_prefix))
}; };
if let Some(hidden) = matches if let Some(hidden) = matches.get_many::<String>("hidden") {
.get_one::<String>("hidden") args.hidden = hidden.cloned().collect();
.map(|v| v.split(',').map(|x| x.to_string()).collect()) } else {
{ let mut hidden = vec![];
args.hidden = hidden; std::mem::swap(&mut args.hidden, &mut hidden);
args.hidden = hidden
.into_iter()
.flat_map(|v| v.split(',').map(|v| v.to_string()).collect::<Vec<String>>())
.collect();
} }
if !args.enable_cors { if !args.enable_cors {
@ -457,8 +464,64 @@ fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, D::E
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let addrs: Vec<&str> = Vec::deserialize(deserializer)?; struct StringOrVec;
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
impl<'de> serde::de::Visitor<'de> for StringOrVec {
type Value = Vec<BindAddr>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
BindAddr::parse_addrs(&[s]).map_err(serde::de::Error::custom)
}
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
let addrs: Vec<&'de str> =
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?;
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_any(StringOrVec)
}
fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec;
impl<'de> serde::de::Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(vec![s.to_owned()])
}
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))
}
}
deserializer.deserialize_any(StringOrVec)
} }
fn deserialize_access_control<'de, D>(deserializer: D) -> Result<AccessControl, D::Error> fn deserialize_access_control<'de, D>(deserializer: D) -> Result<AccessControl, D::Error>
@ -488,3 +551,128 @@ fn default_addrs() -> Vec<BindAddr> {
fn default_port() -> u16 { fn default_port() -> u16 {
5000 5000
} }
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::prelude::*;
#[test]
fn test_default() {
let cli = build_cli();
let matches = cli.try_get_matches_from(vec![""]).unwrap();
let args = Args::parse(matches).unwrap();
let cwd = Args::sanitize_path(std::env::current_dir().unwrap()).unwrap();
assert_eq!(args.serve_path, cwd);
assert_eq!(args.port, default_port());
assert_eq!(args.addrs, default_addrs());
}
#[test]
fn test_args_from_cli1() {
let tmpdir = assert_fs::TempDir::new().unwrap();
let cli = build_cli();
let matches = cli
.try_get_matches_from(vec![
"",
"--hidden",
"tmp,*.log,*.lock",
&tmpdir.to_string_lossy(),
])
.unwrap();
let args = Args::parse(matches).unwrap();
assert_eq!(args.serve_path, Args::sanitize_path(&tmpdir).unwrap());
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
}
#[test]
fn test_args_from_cli2() {
let cli = build_cli();
let matches = cli
.try_get_matches_from(vec![
"", "--hidden", "tmp", "--hidden", "*.log", "--hidden", "*.lock",
])
.unwrap();
let args = Args::parse(matches).unwrap();
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
}
#[test]
fn test_args_from_empty_config_file() {
let tmpdir = assert_fs::TempDir::new().unwrap();
let config_file = tmpdir.child("config.yaml");
config_file.write_str("").unwrap();
let cli = build_cli();
let matches = cli
.try_get_matches_from(vec!["", "-c", &config_file.to_string_lossy()])
.unwrap();
let args = Args::parse(matches).unwrap();
let cwd = Args::sanitize_path(std::env::current_dir().unwrap()).unwrap();
assert_eq!(args.serve_path, cwd);
assert_eq!(args.port, default_port());
assert_eq!(args.addrs, default_addrs());
}
#[test]
fn test_args_from_config_file1() {
let tmpdir = assert_fs::TempDir::new().unwrap();
let config_file = tmpdir.child("config.yaml");
let contents = format!(
r#"
serve-path: {}
bind: 0.0.0.0
port: 3000
allow-upload: true
hidden: tmp,*.log,*.lock
"#,
tmpdir.display()
);
config_file.write_str(&contents).unwrap();
let cli = build_cli();
let matches = cli
.try_get_matches_from(vec!["", "-c", &config_file.to_string_lossy()])
.unwrap();
let args = Args::parse(matches).unwrap();
assert_eq!(args.serve_path, Args::sanitize_path(&tmpdir).unwrap());
assert_eq!(
args.addrs,
vec![BindAddr::Address("0.0.0.0".parse().unwrap())]
);
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
assert_eq!(args.port, 3000);
assert!(args.allow_upload);
}
#[test]
fn test_args_from_config_file2() {
let tmpdir = assert_fs::TempDir::new().unwrap();
let config_file = tmpdir.child("config.yaml");
let contents = r#"
bind:
- 127.0.0.1
- 192.168.8.10
hidden:
- tmp
- '*.log'
- '*.lock'
"#;
config_file.write_str(contents).unwrap();
let cli = build_cli();
let matches = cli
.try_get_matches_from(vec!["", "-c", &config_file.to_string_lossy()])
.unwrap();
let args = Args::parse(matches).unwrap();
assert_eq!(
args.addrs,
vec![
BindAddr::Address("127.0.0.1".parse().unwrap()),
BindAddr::Address("192.168.8.10".parse().unwrap())
]
);
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
}
}

View file

@ -25,7 +25,7 @@ lazy_static! {
}; };
} }
#[derive(Debug)] #[derive(Debug, Clone, PartialEq)]
pub struct AccessControl { pub struct AccessControl {
use_hashed_password: bool, use_hashed_password: bool,
users: IndexMap<String, (String, AccessPaths)>, users: IndexMap<String, (String, AccessPaths)>,

View file

@ -4,7 +4,7 @@ use crate::{auth::get_auth_user, server::Request};
pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#; pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
#[derive(Debug)] #[derive(Debug, Clone, PartialEq)]
pub struct HttpLogger { pub struct HttpLogger {
elements: Vec<LogElement>, elements: Vec<LogElement>,
} }
@ -15,7 +15,7 @@ impl Default for HttpLogger {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, PartialEq)]
enum LogElement { enum LogElement {
Variable(String), Variable(String),
Header(String), Header(String),