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_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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
17
README.md
17
README.md
|
@ -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:
|
||||||
|
|
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::{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"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)>,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue