chore: upgrade clap to v4 (#146)
This commit is contained in:
parent
6ebf619430
commit
1329e42b9a
5 changed files with 482 additions and 249 deletions
571
Cargo.lock
generated
571
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -11,8 +11,8 @@ categories = ["command-line-utilities", "web-programming::http-server"]
|
||||||
keywords = ["static", "file", "server", "webdav", "cli"]
|
keywords = ["static", "file", "server", "webdav", "cli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3", default-features = false, features = ["std", "wrap_help"] }
|
clap = { version = "4", features = ["wrap_help"] }
|
||||||
clap_complete = "3"
|
clap_complete = "4"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
|
||||||
tokio-util = { version = "0.7", features = ["io-util"] }
|
tokio-util = { version = "0.7", features = ["io-util"] }
|
||||||
|
|
53
README.md
53
README.md
|
@ -42,35 +42,34 @@ Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip
|
||||||
```
|
```
|
||||||
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
|
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
|
||||||
|
|
||||||
USAGE:
|
Usage: dufs [OPTIONS] [root]
|
||||||
dufs [OPTIONS] [--] [root]
|
|
||||||
|
|
||||||
ARGS:
|
Arguments:
|
||||||
<root> Specific path to serve [default: .]
|
[root] Specific path to serve [default: .]
|
||||||
|
|
||||||
OPTIONS:
|
Options:
|
||||||
-b, --bind <addr>... Specify bind address or unix socket
|
-b, --bind <addrs> Specify bind address or unix socket
|
||||||
-p, --port <port> Specify port to listen on [default: 5000]
|
-p, --port <port> Specify port to listen on [default: 5000]
|
||||||
--path-prefix <path> Specify a path prefix
|
--path-prefix <path> Specify a path prefix
|
||||||
--hidden <value> Hide paths from directory listings, separated by `,`
|
--hidden <value> Hide paths from directory listings, separated by `,`
|
||||||
-a, --auth <rule>... Add auth for path
|
-a, --auth <rules> Add auth for path
|
||||||
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
|
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
|
||||||
-A, --allow-all Allow all operations
|
-A, --allow-all Allow all operations
|
||||||
--allow-upload Allow upload files/folders
|
--allow-upload Allow upload files/folders
|
||||||
--allow-delete Allow delete files/folders
|
--allow-delete Allow delete files/folders
|
||||||
--allow-search Allow search files/folders
|
--allow-search Allow search files/folders
|
||||||
--allow-symlink Allow symlink to files/folders outside root directory
|
--allow-symlink Allow symlink to files/folders outside root directory
|
||||||
--enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *`
|
--enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *`
|
||||||
--render-index Serve index.html when requesting a directory, returns 404 if not found index.html
|
--render-index Serve index.html when requesting a directory, returns 404 if not found index.html
|
||||||
--render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
|
--render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
|
||||||
--render-spa Serve SPA(Single Page Application)
|
--render-spa Serve SPA(Single Page Application)
|
||||||
--assets <path> Use custom assets to override builtin assets
|
--assets <path> Use custom assets to override builtin assets
|
||||||
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
|
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
|
||||||
--tls-key <path> Path to the SSL/TLS certificate's private key
|
--tls-key <path> Path to the SSL/TLS certificate's private key
|
||||||
--log-format <format> Customize http log format
|
--log-format <format> Customize http log format
|
||||||
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
|
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
-V, --version Print version information
|
-V, --version Print version information
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
86
src/args.rs
86
src/args.rs
|
@ -1,4 +1,5 @@
|
||||||
use clap::{value_parser, AppSettings, Arg, ArgAction, ArgMatches, Command};
|
use clap::builder::PossibleValuesParser;
|
||||||
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
|
||||||
use clap_complete::{generate, Generator, Shell};
|
use clap_complete::{generate, Generator, Shell};
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
use rustls::{Certificate, PrivateKey};
|
use rustls::{Certificate, PrivateKey};
|
||||||
|
@ -14,7 +15,7 @@ use crate::tls::{load_certs, load_private_key};
|
||||||
use crate::utils::encode_uri;
|
use crate::utils::encode_uri;
|
||||||
use crate::BoxResult;
|
use crate::BoxResult;
|
||||||
|
|
||||||
pub fn build_cli() -> Command<'static> {
|
pub fn build_cli() -> Command {
|
||||||
let app = Command::new(env!("CARGO_CRATE_NAME"))
|
let app = Command::new(env!("CARGO_CRATE_NAME"))
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.author(env!("CARGO_PKG_AUTHORS"))
|
.author(env!("CARGO_PKG_AUTHORS"))
|
||||||
|
@ -23,31 +24,30 @@ pub fn build_cli() -> Command<'static> {
|
||||||
" - ",
|
" - ",
|
||||||
env!("CARGO_PKG_REPOSITORY")
|
env!("CARGO_PKG_REPOSITORY")
|
||||||
))
|
))
|
||||||
.global_setting(AppSettings::DeriveDisplayOrder)
|
.arg(
|
||||||
|
Arg::new("root")
|
||||||
|
.default_value(".")
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.help("Specific path to serve"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("bind")
|
Arg::new("bind")
|
||||||
.short('b')
|
.short('b')
|
||||||
.long("bind")
|
.long("bind")
|
||||||
.help("Specify bind address or unix socket")
|
.help("Specify bind address or unix socket")
|
||||||
.multiple_values(true)
|
|
||||||
.value_delimiter(',')
|
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
.value_name("addr"),
|
.value_delimiter(',')
|
||||||
|
.value_name("addrs"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("port")
|
Arg::new("port")
|
||||||
.short('p')
|
.short('p')
|
||||||
.long("port")
|
.long("port")
|
||||||
.default_value("5000")
|
.default_value("5000")
|
||||||
|
.value_parser(value_parser!(u16))
|
||||||
.help("Specify port to listen on")
|
.help("Specify port to listen on")
|
||||||
.value_name("port"),
|
.value_name("port"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new("root")
|
|
||||||
.default_value(".")
|
|
||||||
.allow_invalid_utf8(true)
|
|
||||||
.help("Specific path to serve"),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("path-prefix")
|
Arg::new("path-prefix")
|
||||||
.long("path-prefix")
|
.long("path-prefix")
|
||||||
|
@ -66,15 +66,14 @@ pub fn build_cli() -> Command<'static> {
|
||||||
.long("auth")
|
.long("auth")
|
||||||
.help("Add auth for path")
|
.help("Add auth for path")
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
.multiple_values(true)
|
|
||||||
.value_delimiter(',')
|
.value_delimiter(',')
|
||||||
.value_name("rule"),
|
.value_name("rules"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("auth-method")
|
Arg::new("auth-method")
|
||||||
.long("auth-method")
|
.long("auth-method")
|
||||||
.help("Select auth method")
|
.help("Select auth method")
|
||||||
.possible_values(["basic", "digest"])
|
.value_parser(PossibleValuesParser::new(["basic", "digest"]))
|
||||||
.default_value("digest")
|
.default_value("digest")
|
||||||
.value_name("value"),
|
.value_name("value"),
|
||||||
)
|
)
|
||||||
|
@ -82,53 +81,62 @@ pub fn build_cli() -> Command<'static> {
|
||||||
Arg::new("allow-all")
|
Arg::new("allow-all")
|
||||||
.short('A')
|
.short('A')
|
||||||
.long("allow-all")
|
.long("allow-all")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Allow all operations"),
|
.help("Allow all operations"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("allow-upload")
|
Arg::new("allow-upload")
|
||||||
.long("allow-upload")
|
.long("allow-upload")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Allow upload files/folders"),
|
.help("Allow upload files/folders"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("allow-delete")
|
Arg::new("allow-delete")
|
||||||
.long("allow-delete")
|
.long("allow-delete")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Allow delete files/folders"),
|
.help("Allow delete files/folders"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("allow-search")
|
Arg::new("allow-search")
|
||||||
.long("allow-search")
|
.long("allow-search")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Allow search files/folders"),
|
.help("Allow search files/folders"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("allow-symlink")
|
Arg::new("allow-symlink")
|
||||||
.long("allow-symlink")
|
.long("allow-symlink")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Allow symlink to files/folders outside root directory"),
|
.help("Allow symlink to files/folders outside root directory"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("enable-cors")
|
Arg::new("enable-cors")
|
||||||
.long("enable-cors")
|
.long("enable-cors")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Enable CORS, sets `Access-Control-Allow-Origin: *`"),
|
.help("Enable CORS, sets `Access-Control-Allow-Origin: *`"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("render-index")
|
Arg::new("render-index")
|
||||||
.long("render-index")
|
.long("render-index")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Serve index.html when requesting a directory, returns 404 if not found index.html"),
|
.help("Serve index.html when requesting a directory, returns 404 if not found index.html"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("render-try-index")
|
Arg::new("render-try-index")
|
||||||
.long("render-try-index")
|
.long("render-try-index")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Serve index.html when requesting a directory, returns directory listing if not found index.html"),
|
.help("Serve index.html when requesting a directory, returns directory listing if not found index.html"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("render-spa")
|
Arg::new("render-spa")
|
||||||
.long("render-spa")
|
.long("render-spa")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
.help("Serve SPA(Single Page Application)"),
|
.help("Serve SPA(Single Page Application)"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("assets")
|
Arg::new("assets")
|
||||||
.long("assets")
|
.long("assets")
|
||||||
.help("Use custom assets to override builtin assets")
|
.help("Use custom assets to override builtin assets")
|
||||||
.allow_invalid_utf8(true)
|
.value_parser(value_parser!(PathBuf))
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -138,12 +146,14 @@ pub fn build_cli() -> Command<'static> {
|
||||||
Arg::new("tls-cert")
|
Arg::new("tls-cert")
|
||||||
.long("tls-cert")
|
.long("tls-cert")
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
.help("Path to an SSL/TLS certificate to serve with HTTPS"),
|
.help("Path to an SSL/TLS certificate to serve with HTTPS"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("tls-key")
|
Arg::new("tls-key")
|
||||||
.long("tls-key")
|
.long("tls-key")
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
.help("Path to the SSL/TLS certificate's private key"),
|
.help("Path to the SSL/TLS certificate's private key"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -199,16 +209,16 @@ impl Args {
|
||||||
/// If a parsing error ocurred, exit the process and print out informative
|
/// If a parsing error ocurred, exit the process and print out informative
|
||||||
/// error message to user.
|
/// error message to user.
|
||||||
pub fn parse(matches: ArgMatches) -> BoxResult<Args> {
|
pub fn parse(matches: ArgMatches) -> BoxResult<Args> {
|
||||||
let port = matches.value_of_t::<u16>("port")?;
|
let port = *matches.get_one::<u16>("port").unwrap();
|
||||||
let addrs = matches
|
let addrs = matches
|
||||||
.values_of("bind")
|
.get_many::<String>("bind")
|
||||||
.map(|v| v.collect())
|
.map(|bind| bind.map(|v| v.as_str()).collect())
|
||||||
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
|
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
|
||||||
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?;
|
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?;
|
||||||
let path = Args::parse_path(matches.value_of_os("root").unwrap_or_default())?;
|
let path = Args::parse_path(matches.get_one::<PathBuf>("root").unwrap())?;
|
||||||
let path_is_file = path.metadata()?.is_file();
|
let path_is_file = path.metadata()?.is_file();
|
||||||
let path_prefix = matches
|
let path_prefix = matches
|
||||||
.value_of("path-prefix")
|
.get_one::<String>("path-prefix")
|
||||||
.map(|v| v.trim_matches('/').to_owned())
|
.map(|v| v.trim_matches('/').to_owned())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let uri_prefix = if path_prefix.is_empty() {
|
let uri_prefix = if path_prefix.is_empty() {
|
||||||
|
@ -217,28 +227,31 @@ impl Args {
|
||||||
format!("/{}/", &encode_uri(&path_prefix))
|
format!("/{}/", &encode_uri(&path_prefix))
|
||||||
};
|
};
|
||||||
let hidden: Vec<String> = matches
|
let hidden: Vec<String> = matches
|
||||||
.value_of("hidden")
|
.get_one::<String>("hidden")
|
||||||
.map(|v| v.split(',').map(|x| x.to_string()).collect())
|
.map(|v| v.split(',').map(|x| x.to_string()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let enable_cors = matches.is_present("enable-cors");
|
let enable_cors = matches.get_flag("enable-cors");
|
||||||
let auth: Vec<&str> = matches
|
let auth: Vec<&str> = matches
|
||||||
.values_of("auth")
|
.get_many::<String>("auth")
|
||||||
.map(|v| v.collect())
|
.map(|auth| auth.map(|v| v.as_str()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let auth_method = match matches.value_of("auth-method").unwrap() {
|
let auth_method = match matches.get_one::<String>("auth-method").unwrap().as_str() {
|
||||||
"basic" => AuthMethod::Basic,
|
"basic" => AuthMethod::Basic,
|
||||||
_ => AuthMethod::Digest,
|
_ => AuthMethod::Digest,
|
||||||
};
|
};
|
||||||
let auth = AccessControl::new(&auth, &uri_prefix)?;
|
let auth = AccessControl::new(&auth, &uri_prefix)?;
|
||||||
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
|
let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload");
|
||||||
let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
|
let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete");
|
||||||
let allow_search = matches.is_present("allow-all") || matches.is_present("allow-search");
|
let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search");
|
||||||
let allow_symlink = matches.is_present("allow-all") || matches.is_present("allow-symlink");
|
let allow_symlink = matches.get_flag("allow-all") || matches.get_flag("allow-symlink");
|
||||||
let render_index = matches.is_present("render-index");
|
let render_index = matches.get_flag("render-index");
|
||||||
let render_try_index = matches.is_present("render-try-index");
|
let render_try_index = matches.get_flag("render-try-index");
|
||||||
let render_spa = matches.is_present("render-spa");
|
let render_spa = matches.get_flag("render-spa");
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
let tls = match (matches.value_of("tls-cert"), matches.value_of("tls-key")) {
|
let tls = match (
|
||||||
|
matches.get_one::<PathBuf>("tls-cert"),
|
||||||
|
matches.get_one::<PathBuf>("tls-key"),
|
||||||
|
) {
|
||||||
(Some(certs_file), Some(key_file)) => {
|
(Some(certs_file), Some(key_file)) => {
|
||||||
let certs = load_certs(certs_file)?;
|
let certs = load_certs(certs_file)?;
|
||||||
let key = load_private_key(key_file)?;
|
let key = load_private_key(key_file)?;
|
||||||
|
@ -249,10 +262,11 @@ impl Args {
|
||||||
#[cfg(not(feature = "tls"))]
|
#[cfg(not(feature = "tls"))]
|
||||||
let tls = None;
|
let tls = None;
|
||||||
let log_http: LogHttp = matches
|
let log_http: LogHttp = matches
|
||||||
.value_of("log-format")
|
.get_one::<String>("log-format")
|
||||||
|
.map(|v| v.as_str())
|
||||||
.unwrap_or(DEFAULT_LOG_FORMAT)
|
.unwrap_or(DEFAULT_LOG_FORMAT)
|
||||||
.parse()?;
|
.parse()?;
|
||||||
let assets_path = match matches.value_of_os("assets") {
|
let assets_path = match matches.get_one::<PathBuf>("assets") {
|
||||||
Some(v) => Some(Args::parse_assets_path(v)?),
|
Some(v) => Some(Args::parse_assets_path(v)?),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
17
src/tls.rs
17
src/tls.rs
|
@ -5,6 +5,7 @@ use hyper::server::conn::{AddrIncoming, AddrStream};
|
||||||
use rustls::{Certificate, PrivateKey};
|
use rustls::{Certificate, PrivateKey};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::Path;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
|
@ -123,10 +124,12 @@ impl Accept for TlsAcceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load public certificate from file.
|
// Load public certificate from file.
|
||||||
pub fn load_certs(filename: &str) -> Result<Vec<Certificate>, Box<dyn std::error::Error>> {
|
pub fn load_certs<T: AsRef<Path>>(
|
||||||
|
filename: T,
|
||||||
|
) -> Result<Vec<Certificate>, Box<dyn std::error::Error>> {
|
||||||
// Open certificate file.
|
// Open certificate file.
|
||||||
let cert_file =
|
let cert_file = fs::File::open(filename.as_ref())
|
||||||
fs::File::open(filename).map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
|
.map_err(|e| format!("Failed to access `{}`, {}", filename.as_ref().display(), e))?;
|
||||||
let mut reader = io::BufReader::new(cert_file);
|
let mut reader = io::BufReader::new(cert_file);
|
||||||
|
|
||||||
// Load and return certificate.
|
// Load and return certificate.
|
||||||
|
@ -138,9 +141,11 @@ pub fn load_certs(filename: &str) -> Result<Vec<Certificate>, Box<dyn std::error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load private key from file.
|
// Load private key from file.
|
||||||
pub fn load_private_key(filename: &str) -> Result<PrivateKey, Box<dyn std::error::Error>> {
|
pub fn load_private_key<T: AsRef<Path>>(
|
||||||
let key_file =
|
filename: T,
|
||||||
fs::File::open(filename).map_err(|e| format!("Failed to access `{}`, {}", &filename, e))?;
|
) -> Result<PrivateKey, Box<dyn std::error::Error>> {
|
||||||
|
let key_file = fs::File::open(filename.as_ref())
|
||||||
|
.map_err(|e| format!("Failed to access `{}`, {}", filename.as_ref().display(), e))?;
|
||||||
let mut reader = io::BufReader::new(key_file);
|
let mut reader = io::BufReader::new(key_file);
|
||||||
|
|
||||||
// Load and return a single private key.
|
// Load and return a single private key.
|
||||||
|
|
Loading…
Reference in a new issue