feat: support config file with --config option (#281)

This commit is contained in:
sigoden
2023-11-04 16:58:19 +08:00
committed by GitHub
parent 5782c5f413
commit 4ef07737e1
12 changed files with 434 additions and 180 deletions

View File

@@ -2,16 +2,13 @@ use anyhow::{bail, Context, Result};
use clap::builder::PossibleValuesParser;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use clap_complete::{generate, Generator, Shell};
#[cfg(feature = "tls")]
use rustls::{Certificate, PrivateKey};
use serde::{Deserialize, Deserializer};
use std::env;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use crate::auth::AccessControl;
use crate::log_http::{LogHttp, DEFAULT_LOG_FORMAT};
#[cfg(feature = "tls")]
use crate::tls::{load_certs, load_private_key};
use crate::log_http::LogHttp;
use crate::utils::encode_uri;
pub fn build_cli() -> Command {
@@ -24,12 +21,20 @@ pub fn build_cli() -> Command {
env!("CARGO_PKG_REPOSITORY")
))
.arg(
Arg::new("serve_path")
Arg::new("serve-path")
.env("DUFS_SERVE_PATH")
.hide_env(true)
.default_value(".")
.value_parser(value_parser!(PathBuf))
.help("Specific path to serve"),
.help("Specific path to serve [default: .]"),
)
.arg(
Arg::new("config")
.env("DUFS_SERVE_PATH")
.hide_env(true)
.short('c')
.long("config")
.value_parser(value_parser!(PathBuf))
.help("Specify configuration file"),
)
.arg(
Arg::new("bind")
@@ -48,9 +53,8 @@ pub fn build_cli() -> Command {
.hide_env(true)
.short('p')
.long("port")
.default_value("5000")
.value_parser(value_parser!(u16))
.help("Specify port to listen on")
.help("Specify port to listen on [default: 5000]")
.value_name("port"),
)
.arg(
@@ -66,7 +70,7 @@ pub fn build_cli() -> Command {
.env("DUFS_HIDDEN")
.hide_env(true)
.long("hidden")
.help("Hide paths from directory listings, separated by `,`")
.help("Hide paths from directory listings, e.g. tmp,*.log,*.lock")
.value_name("value"),
)
.arg(
@@ -177,9 +181,24 @@ pub fn build_cli() -> Command {
.env("DUFS_ASSETS")
.hide_env(true)
.long("assets")
.help("Use custom assets to override builtin assets")
.help("Set the path to the assets directory for overriding the built-in assets")
.value_parser(value_parser!(PathBuf))
.value_name("path")
)
.arg(
Arg::new("log-format")
.env("DUFS_LOG_FORMAT")
.hide_env(true)
.long("log-format")
.value_name("format")
.help("Customize http log format"),
)
.arg(
Arg::new("completions")
.long("completions")
.value_name("shell")
.value_parser(value_parser!(Shell))
.help("Print shell completion script for <shell>"),
);
#[cfg(feature = "tls")]
@@ -203,37 +222,32 @@ pub fn build_cli() -> Command {
.help("Path to the SSL/TLS certificate's private key"),
);
app.arg(
Arg::new("log-format")
.env("DUFS_LOG_FORMAT")
.hide_env(true)
.long("log-format")
.value_name("format")
.help("Customize http log format"),
)
.arg(
Arg::new("completions")
.long("completions")
.value_name("shell")
.value_parser(value_parser!(Shell))
.help("Print shell completion script for <shell>"),
)
app
}
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)]
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
#[serde(rename_all = "kebab-case")]
pub struct Args {
#[serde(default = "default_serve_path")]
pub serve_path: PathBuf,
#[serde(deserialize_with = "deserialize_bind_addrs")]
#[serde(rename = "bind")]
pub addrs: Vec<BindAddr>,
pub port: u16,
pub path: PathBuf,
#[serde(skip)]
pub path_is_file: bool,
pub path_prefix: String,
#[serde(skip)]
pub uri_prefix: String,
pub hidden: Vec<String>,
#[serde(deserialize_with = "deserialize_access_control")]
pub auth: AccessControl,
pub allow_all: bool,
pub allow_upload: bool,
pub allow_delete: bool,
pub allow_search: bool,
@@ -243,12 +257,12 @@ pub struct Args {
pub render_spa: bool,
pub render_try_index: bool,
pub enable_cors: bool,
pub assets_path: Option<PathBuf>,
pub assets: Option<PathBuf>,
#[serde(deserialize_with = "deserialize_log_http")]
#[serde(rename = "log-format")]
pub log_http: LogHttp,
#[cfg(feature = "tls")]
pub tls: Option<(Vec<Certificate>, PrivateKey)>,
#[cfg(not(feature = "tls"))]
pub tls: Option<()>,
pub tls_cert: Option<PathBuf>,
pub tls_key: Option<PathBuf>,
}
impl Args {
@@ -257,90 +271,164 @@ 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 port = *matches.get_one::<u16>("port").unwrap();
let addrs = matches
.get_many::<String>("bind")
.map(|bind| bind.map(|v| v.as_str()).collect())
.unwrap_or_else(|| vec!["0.0.0.0", "::"]);
let addrs: Vec<BindAddr> = Args::parse_addrs(&addrs)?;
let path = Args::parse_path(matches.get_one::<PathBuf>("serve_path").unwrap())?;
let path_is_file = path.metadata()?.is_file();
let path_prefix = matches
.get_one::<String>("path-prefix")
.map(|v| v.trim_matches('/').to_owned())
.unwrap_or_default();
let uri_prefix = if path_prefix.is_empty() {
let mut args = Self {
serve_path: default_serve_path(),
addrs: BindAddr::parse_addrs(&["0.0.0.0", "::"]).unwrap(),
port: 5000,
..Default::default()
};
if let Some(config_path) = matches.get_one::<PathBuf>("config") {
let contents = std::fs::read_to_string(config_path)
.with_context(|| format!("Failed to read config at {}", config_path.display()))?;
args = serde_yaml::from_str(&contents)
.with_context(|| format!("Failed to load config at {}", config_path.display()))?;
}
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") {
args.port = *port
}
if let Some(addrs) = matches.get_many::<String>("bind") {
let addrs: Vec<_> = addrs.map(|v| v.as_str()).collect();
args.addrs = BindAddr::parse_addrs(&addrs)?;
}
args.path_is_file = args.serve_path.metadata()?.is_file();
if let Some(path_prefix) = matches.get_one::<String>("path-prefix") {
args.path_prefix = path_prefix.clone();
}
args.path_prefix = args.path_prefix.trim_matches('/').to_string();
args.uri_prefix = if args.path_prefix.is_empty() {
"/".to_owned()
} else {
format!("/{}/", &encode_uri(&path_prefix))
format!("/{}/", &encode_uri(&args.path_prefix))
};
let hidden: Vec<String> = matches
if let Some(hidden) = matches
.get_one::<String>("hidden")
.map(|v| v.split(',').map(|x| x.to_string()).collect())
.unwrap_or_default();
let enable_cors = matches.get_flag("enable-cors");
let auth: Vec<&str> = matches
.get_many::<String>("auth")
.map(|auth| auth.map(|v| v.as_str()).collect())
.unwrap_or_default();
let auth = AccessControl::new(&auth)?;
let allow_upload = matches.get_flag("allow-all") || matches.get_flag("allow-upload");
let allow_delete = matches.get_flag("allow-all") || matches.get_flag("allow-delete");
let allow_search = matches.get_flag("allow-all") || matches.get_flag("allow-search");
let allow_symlink = matches.get_flag("allow-all") || matches.get_flag("allow-symlink");
let allow_archive = matches.get_flag("allow-all") || matches.get_flag("allow-archive");
let render_index = matches.get_flag("render-index");
let render_try_index = matches.get_flag("render-try-index");
let render_spa = matches.get_flag("render-spa");
#[cfg(feature = "tls")]
let tls = match (
matches.get_one::<PathBuf>("tls-cert"),
matches.get_one::<PathBuf>("tls-key"),
) {
(Some(certs_file), Some(key_file)) => {
let certs = load_certs(certs_file)?;
let key = load_private_key(key_file)?;
Some((certs, key))
}
_ => None,
};
#[cfg(not(feature = "tls"))]
let tls = None;
let log_http: LogHttp = matches
.get_one::<String>("log-format")
.map(|v| v.as_str())
.unwrap_or(DEFAULT_LOG_FORMAT)
.parse()?;
let assets_path = match matches.get_one::<PathBuf>("assets") {
Some(v) => Some(Args::parse_assets_path(v)?),
None => None,
};
{
args.hidden = hidden;
}
Ok(Args {
addrs,
port,
path,
path_is_file,
path_prefix,
uri_prefix,
hidden,
auth,
enable_cors,
allow_delete,
allow_upload,
allow_search,
allow_symlink,
allow_archive,
render_index,
render_try_index,
render_spa,
tls,
log_http,
assets_path,
})
if !args.enable_cors {
args.enable_cors = matches.get_flag("enable-cors");
}
if let Some(rules) = matches.get_many::<String>("auth") {
let rules: Vec<_> = rules.map(|v| v.as_str()).collect();
args.auth = AccessControl::new(&rules)?;
}
if !args.allow_all {
args.allow_all = matches.get_flag("allow-all");
}
let allow_all = args.allow_all;
if !args.allow_upload {
args.allow_upload = allow_all || matches.get_flag("allow-upload");
}
if !args.allow_delete {
args.allow_delete = allow_all || matches.get_flag("allow-delete");
}
if !args.allow_search {
args.allow_search = allow_all || matches.get_flag("allow-search");
}
if !args.allow_symlink {
args.allow_symlink = allow_all || matches.get_flag("allow-symlink");
}
if !args.allow_archive {
args.allow_archive = allow_all || matches.get_flag("allow-archive");
}
if !args.render_index {
args.render_index = matches.get_flag("render-index");
}
if !args.render_try_index {
args.render_try_index = matches.get_flag("render-try-index");
}
if !args.render_spa {
args.render_spa = matches.get_flag("render-spa");
}
if let Some(log_format) = matches.get_one::<String>("log-format") {
args.log_http = log_format.parse()?;
}
if let Some(assets_path) = matches.get_one::<PathBuf>("assets") {
args.assets = Some(assets_path.clone());
}
if let Some(assets_path) = &args.assets {
args.assets = Some(Args::sanitize_assets_path(assets_path)?);
}
#[cfg(feature = "tls")]
{
if let Some(tls_cert) = matches.get_one::<PathBuf>("tls-cert") {
args.tls_cert = Some(tls_cert.clone())
}
if let Some(tls_key) = matches.get_one::<PathBuf>("tls-key") {
args.tls_key = Some(tls_key.clone())
}
match (&args.tls_cert, &args.tls_key) {
(Some(_), Some(_)) => {}
(Some(_), _) => bail!("No tls-key set"),
(_, Some(_)) => bail!("No tls-cert set"),
(None, None) => {}
}
}
#[cfg(not(feature = "tls"))]
{
args.tls_cert = None;
args.tls_key = None;
}
Ok(args)
}
fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
fn sanitize_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
if !path.exists() {
bail!("Path `{}` doesn't exist", path.display());
}
env::current_dir()
.and_then(|mut p| {
p.push(path); // If path is absolute, it replaces the current path.
std::fs::canonicalize(p)
})
.with_context(|| format!("Failed to access path `{}`", path.display()))
}
fn sanitize_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = Self::sanitize_path(path)?;
if !path.join("index.html").exists() {
bail!("Path `{}` doesn't contains index.html", path.display());
}
Ok(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BindAddr {
Address(IpAddr),
Path(PathBuf),
}
impl BindAddr {
fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
let mut bind_addrs = vec![];
let mut invalid_addrs = vec![];
for addr in addrs {
@@ -362,32 +450,32 @@ impl Args {
}
Ok(bind_addrs)
}
fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = path.as_ref();
if !path.exists() {
bail!("Path `{}` doesn't exist", path.display());
}
env::current_dir()
.and_then(|mut p| {
p.push(path); // If path is absolute, it replaces the current path.
std::fs::canonicalize(p)
})
.with_context(|| format!("Failed to access path `{}`", path.display()))
}
fn parse_assets_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let path = Self::parse_path(path)?;
if !path.join("index.html").exists() {
bail!("Path `{}` doesn't contains index.html", path.display());
}
Ok(path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BindAddr {
Address(IpAddr),
Path(PathBuf),
fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result<Vec<BindAddr>, D::Error>
where
D: Deserializer<'de>,
{
let addrs: Vec<&str> = Vec::deserialize(deserializer)?;
BindAddr::parse_addrs(&addrs).map_err(serde::de::Error::custom)
}
fn deserialize_access_control<'de, D>(deserializer: D) -> Result<AccessControl, D::Error>
where
D: Deserializer<'de>,
{
let rules: Vec<&str> = Vec::deserialize(deserializer)?;
AccessControl::new(&rules).map_err(serde::de::Error::custom)
}
fn deserialize_log_http<'de, D>(deserializer: D) -> Result<LogHttp, D::Error>
where
D: Deserializer<'de>,
{
let value: String = Deserialize::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
fn default_serve_path() -> PathBuf {
PathBuf::from(".")
}