feat: more flexible config values (#299)
This commit is contained in:
parent
7584fe3d08
commit
6ff8b29b69
6 changed files with 226 additions and 26 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -492,6 +492,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha-crypt",
|
||||
"smart-default",
|
||||
"socket2 0.5.5",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
|
@ -1552,6 +1553,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "socket2"
|
||||
version = "0.4.10"
|
||||
|
|
|
@ -47,6 +47,7 @@ indexmap = "2.0"
|
|||
serde_yaml = "0.9.27"
|
||||
sha-crypt = "0.5.0"
|
||||
base64 = "0.21.5"
|
||||
smart-default = "0.7.1"
|
||||
|
||||
[features]
|
||||
default = ["tls"]
|
||||
|
|
17
README.md
17
README.md
|
@ -235,8 +235,7 @@ $6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8
|
|||
|
||||
Use hashed password
|
||||
```
|
||||
dufs \
|
||||
-a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw'
|
||||
dufs -a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```sh
|
||||
dufs --hidden '.*' # hidden dotfiles
|
||||
dufs --hidden '*/' # hidden all folders
|
||||
dufs --hidden '*.log,*.lock' # hidden by exts
|
||||
dufs --hidden '.*' # hidden dotfiles
|
||||
dufs --hidden '*/' # hidden all folders
|
||||
dufs --hidden '*.log,*.lock' # hidden by exts
|
||||
dufs --hidden '*.log' --hidden '*.lock'
|
||||
```
|
||||
|
||||
### Log Format
|
||||
|
@ -311,8 +311,8 @@ All options can be set using environment variables prefixed with `DUFS_`.
|
|||
--config <path> DUFS_CONFIG=config.yaml
|
||||
-b, --bind <addrs> DUFS_BIND=0.0.0.0
|
||||
-p, --port <port> DUFS_PORT=5000
|
||||
--path-prefix <path> DUFS_PATH_PREFIX=/path
|
||||
--hidden <value> DUFS_HIDDEN=*.log
|
||||
--path-prefix <path> DUFS_PATH_PREFIX=/static
|
||||
--hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock
|
||||
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
|
||||
-A, --allow-all DUFS_ALLOW_ALL=true
|
||||
--allow-upload DUFS_ALLOW_UPLOAD=true
|
||||
|
@ -338,8 +338,7 @@ The following are the configuration items:
|
|||
|
||||
```yaml
|
||||
serve-path: '.'
|
||||
bind:
|
||||
- 192.168.8.10
|
||||
bind: 0.0.0.0
|
||||
port: 5000
|
||||
path-prefix: /dufs
|
||||
hidden:
|
||||
|
|
216
src/args.rs
216
src/args.rs
|
@ -3,6 +3,7 @@ use clap::builder::PossibleValuesParser;
|
|||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||
use clap_complete::{generate, Generator, Shell};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use smart_default::SmartDefault;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -70,6 +71,8 @@ pub fn build_cli() -> Command {
|
|||
.env("DUFS_HIDDEN")
|
||||
.hide_env(true)
|
||||
.long("hidden")
|
||||
.action(ArgAction::Append)
|
||||
.value_delimiter(',')
|
||||
.help("Hide paths from directory listings, e.g. tmp,*.log,*.lock")
|
||||
.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());
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Debug, Deserialize, SmartDefault, PartialEq)]
|
||||
#[serde(default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Args {
|
||||
#[serde(default = "default_serve_path")]
|
||||
#[default(default_serve_path())]
|
||||
pub serve_path: PathBuf,
|
||||
#[serde(deserialize_with = "deserialize_bind_addrs")]
|
||||
#[serde(rename = "bind")]
|
||||
#[serde(default = "default_addrs")]
|
||||
#[default(default_addrs())]
|
||||
pub addrs: Vec<BindAddr>,
|
||||
#[serde(default = "default_port")]
|
||||
#[default(default_port())]
|
||||
pub port: u16,
|
||||
#[serde(skip)]
|
||||
pub path_is_file: bool,
|
||||
pub path_prefix: String,
|
||||
#[serde(skip)]
|
||||
pub uri_prefix: String,
|
||||
#[serde(deserialize_with = "deserialize_string_or_vec")]
|
||||
pub hidden: Vec<String>,
|
||||
#[serde(deserialize_with = "deserialize_access_control")]
|
||||
pub auth: AccessControl,
|
||||
|
@ -272,12 +279,7 @@ impl Args {
|
|||
/// If a parsing error occurred, exit the process and print out informative
|
||||
/// error message to user.
|
||||
pub fn parse(matches: ArgMatches) -> Result<Args> {
|
||||
let mut args = Self {
|
||||
serve_path: default_serve_path(),
|
||||
addrs: default_addrs(),
|
||||
port: default_port(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut args = Self::default();
|
||||
|
||||
if let Some(config_path) = matches.get_one::<PathBuf>("config") {
|
||||
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") {
|
||||
args.serve_path = path.clone()
|
||||
}
|
||||
|
||||
args.serve_path = Self::sanitize_path(args.serve_path)?;
|
||||
|
||||
if let Some(port) = matches.get_one::<u16>("port") {
|
||||
|
@ -312,11 +315,15 @@ impl Args {
|
|||
format!("/{}/", &encode_uri(&args.path_prefix))
|
||||
};
|
||||
|
||||
if let Some(hidden) = matches
|
||||
.get_one::<String>("hidden")
|
||||
.map(|v| v.split(',').map(|x| x.to_string()).collect())
|
||||
{
|
||||
args.hidden = hidden;
|
||||
if let Some(hidden) = matches.get_many::<String>("hidden") {
|
||||
args.hidden = hidden.cloned().collect();
|
||||
} else {
|
||||
let mut hidden = vec![];
|
||||
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 {
|
||||
|
@ -457,8 +464,64 @@ fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, D::E
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let addrs: Vec<&str> = Vec::deserialize(deserializer)?;
|
||||
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
|
||||
struct StringOrVec;
|
||||
|
||||
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>
|
||||
|
@ -488,3 +551,128 @@ fn default_addrs() -> Vec<BindAddr> {
|
|||
fn default_port() -> u16 {
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ lazy_static! {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AccessControl {
|
||||
use_hashed_password: bool,
|
||||
users: IndexMap<String, (String, AccessPaths)>,
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{auth::get_auth_user, server::Request};
|
|||
|
||||
pub const DEFAULT_LOG_FORMAT: &str = r#"$remote_addr "$request" $status"#;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HttpLogger {
|
||||
elements: Vec<LogElement>,
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ impl Default for HttpLogger {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum LogElement {
|
||||
Variable(String),
|
||||
Header(String),
|
||||
|
|
Loading…
Reference in a new issue