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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 434 additions and 180 deletions

28
Cargo.lock generated
View file

@ -481,6 +481,7 @@ dependencies = [
"rustls-pemfile",
"serde",
"serde_json",
"serde_yaml",
"socket2 0.5.3",
"tokio",
"tokio-rustls",
@ -1488,18 +1489,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.186"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.186"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
dependencies = [
"proc-macro2",
"quote",
@ -1529,6 +1530,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.5"
@ -1808,6 +1822,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]]
name = "untrusted"
version = "0.7.1"

View file

@ -45,6 +45,7 @@ anyhow = "1.0"
chardetng = "0.1"
glob = "0.3.1"
indexmap = "2.0"
serde_yaml = "0.9.27"
[features]
default = ["tls"]

View file

@ -48,16 +48,17 @@ Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip
```
Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
Usage: dufs [OPTIONS] [serve_path]
Usage: dufs [OPTIONS] [serve-path]
Arguments:
[serve_path] Specific path to serve [default: .]
[serve-path] Specific path to serve [default: .]
Options:
-c, --config <config> Specify configuration file
-b, --bind <addrs> Specify bind address or unix socket
-p, --port <port> Specify port to listen on [default: 5000]
--path-prefix <path> Specify a path prefix
--hidden <value> Hide paths from directory listings, separated by `,`
--hidden <value> Hide paths from directory listings, e.g. tmp,*.log,*.lock
-a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2
-A, --allow-all Allow all operations
--allow-upload Allow upload files/folders
@ -69,11 +70,11 @@ Options:
--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-spa Serve SPA(Single Page Application)
--assets <path> Use custom assets to override builtin assets
--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
--assets <path> Set the path to the assets directory for overriding the built-in assets
--log-format <format> Customize http log format
--completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
--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
-h, --help Print help
-V, --version Print version
```
@ -308,7 +309,7 @@ dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admi
All options can be set using environment variables prefixed with `DUFS_`.
```
[SERVE_PATH] DUFS_SERVE_PATH=/dir
[serve-path] DUFS_SERVE_PATH=/dir
-b, --bind <addrs> DUFS_BIND=0.0.0.0
-p, --port <port> DUFS_PORT=5000
--path-prefix <path> DUFS_PATH_PREFIX=/path
@ -325,9 +326,44 @@ All options can be set using environment variables prefixed with `DUFS_`.
--render-try-index DUFS_RENDER_TRY_INDEX=true
--render-spa DUFS_RENDER_SPA=true
--assets <path> DUFS_ASSETS=/assets
--log-format <format> DUFS_LOG_FORMAT=""
--tls-cert <path> DUFS_TLS_CERT=cert.pem
--tls-key <path> DUFS_TLS_KEY=key.pem
--log-format <format> DUFS_LOG_FORMAT=""
```
## Configuration File
You can specify and use the configuration file by selecting the option `--config <path-to-config.yaml>`.
The following are the configuration items:
```yaml
server-path: '.'
bind:
- 192.168.8.10
port: 5000
path-prefix: /dufs
hidden:
- tmp
- '*.log'
- '*.lock'
auth:
- admin:admin@/:rw
- user:pass@/src:rw,/share
allow-all: false
allow-upload: true
allow-delete: true
allow-search: true
allow-symlink: true
allow-archive: true
enable-cors: true
render-index: true
render-try-index: true
render-spa: true
assets: ./assets/
log-format: '$remote_addr "$request" $status $http_user_agent'
tls-cert: tests/data/cert.pem
tls-key: tests/data/key_pkcs1.pem
```
### Customize UI

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");
{
args.hidden = hidden;
}
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")]
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))
{
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) => {}
}
}
_ => 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,
};
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,
})
{
args.tls_cert = None;
args.tls_key = None;
}
fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
Ok(args)
}
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(".")
}

View file

@ -25,21 +25,26 @@ lazy_static! {
};
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct AccessControl {
users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>,
}
impl Default for AccessControl {
fn default() -> Self {
AccessControl {
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
}
}
}
impl AccessControl {
pub fn new(raw_rules: &[&str]) -> Result<Self> {
if raw_rules.is_empty() {
return Ok(AccessControl {
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
});
return Ok(Default::default());
}
let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
let mut anony = None;
let mut anony_paths = vec![];

View file

@ -9,6 +9,12 @@ pub struct LogHttp {
elements: Vec<LogElement>,
}
impl Default for LogHttp {
fn default() -> Self {
DEFAULT_LOG_FORMAT.parse().unwrap()
}
}
#[derive(Debug)]
enum LogElement {
Variable(String),

View file

@ -16,7 +16,7 @@ extern crate log;
use crate::args::{build_cli, print_completions, Args};
use crate::server::{Request, Server};
#[cfg(feature = "tls")]
use crate::tls::{TlsAcceptor, TlsStream};
use crate::tls::{load_certs, load_private_key, TlsAcceptor, TlsStream};
use anyhow::{anyhow, Context, Result};
use std::net::{IpAddr, SocketAddr, TcpListener as StdTcpListener};
@ -88,9 +88,12 @@ fn serve(
BindAddr::Address(ip) => {
let incoming = create_addr_incoming(SocketAddr::new(*ip, port))
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
match args.tls.as_ref() {
match (&args.tls_cert, &args.tls_key) {
#[cfg(feature = "tls")]
Some((certs, key)) => {
(Some(cert_file), Some(key_file)) => {
let certs = load_certs(cert_file)?;
let key = load_private_key(key_file)?;
let config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
@ -105,11 +108,7 @@ fn serve(
tokio::spawn(hyper::Server::builder(accepter).serve(new_service));
handles.push(server);
}
#[cfg(not(feature = "tls"))]
Some(_) => {
unreachable!()
}
None => {
(None, None) => {
let new_service = make_service_fn(move |socket: &AddrStream| {
let remote_addr = socket.remote_addr();
serve_func(Some(remote_addr))
@ -118,6 +117,9 @@ fn serve(
tokio::spawn(hyper::Server::builder(incoming).serve(new_service));
handles.push(server);
}
_ => {
unreachable!()
}
};
}
BindAddr::Path(path) => {
@ -195,7 +197,11 @@ fn print_listening(args: Arc<Args>) -> Result<()> {
IpAddr::V4(_) => format!("{}:{}", addr, args.port),
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
};
let protocol = if args.tls.is_some() { "https" } else { "http" };
let protocol = if args.tls_cert.is_some() {
"https"
} else {
"http"
};
format!("{}://{}{}", protocol, addr, args.uri_prefix)
}
BindAddr::Path(path) => path.display().to_string(),

View file

@ -71,13 +71,13 @@ impl Server {
encode_uri(&format!(
"{}{}",
&args.uri_prefix,
get_file_name(&args.path)
get_file_name(&args.serve_path)
)),
]
} else {
vec![]
};
let html = match args.assets_path.as_ref() {
let html = match args.assets.as_ref() {
Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
None => Cow::Borrowed(INDEX_HTML),
};
@ -180,7 +180,7 @@ impl Server {
.iter()
.any(|v| v.as_str() == req_path)
{
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
self.handle_send_file(&self.args.serve_path, headers, head_only, &mut res)
.await?;
} else {
status_not_found(&mut res);
@ -620,7 +620,7 @@ impl Server {
res: &mut Response,
) -> Result<()> {
if path.extension().is_none() {
let path = self.args.path.join(INDEX_NAME);
let path = self.args.serve_path.join(INDEX_NAME);
self.handle_send_file(&path, headers, head_only, res)
.await?;
} else {
@ -636,7 +636,7 @@ impl Server {
res: &mut Response,
) -> Result<bool> {
if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
match self.args.assets_path.as_ref() {
match self.args.assets.as_ref() {
Some(assets_path) => {
let path = assets_path.join(name);
self.handle_send_file(&path, headers, false, res).await?;
@ -776,7 +776,10 @@ impl Server {
) -> Result<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
let (file, meta) = (file?, meta?);
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let href = format!(
"/{}",
normalize_path(path.strip_prefix(&self.args.serve_path)?)
);
let mut buffer: Vec<u8> = vec![];
file.take(1024).read_to_end(&mut buffer).await?;
let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
@ -822,12 +825,15 @@ impl Server {
},
None => 1,
};
let mut paths = match self.to_pathitem(path, &self.args.path).await? {
let mut paths = match self.to_pathitem(path, &self.args.serve_path).await? {
Some(v) => vec![v],
None => vec![],
};
if depth != 0 {
match self.list_dir(path, &self.args.path, access_paths).await {
match self
.list_dir(path, &self.args.serve_path, access_paths)
.await
{
Ok(child) => paths.extend(child),
Err(_) => {
status_forbid(res);
@ -847,7 +853,7 @@ impl Server {
}
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
if let Some(pathitem) = self.to_pathitem(path, &self.args.serve_path).await? {
res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
} else {
status_not_found(res);
@ -990,7 +996,10 @@ impl Server {
}
return Ok(());
}
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let href = format!(
"/{}",
normalize_path(path.strip_prefix(&self.args.serve_path)?)
);
let readwrite = access_paths.perm().readwrite();
let data = IndexData {
kind: DataKind::Index,
@ -1038,7 +1047,7 @@ impl Server {
fs::canonicalize(path)
.await
.ok()
.map(|v| v.starts_with(&self.args.path))
.map(|v| v.starts_with(&self.args.serve_path))
.unwrap_or_default()
}
@ -1104,14 +1113,14 @@ impl Server {
fn join_path(&self, path: &str) -> Option<PathBuf> {
if path.is_empty() {
return Some(self.args.path.clone());
return Some(self.args.serve_path.clone());
}
let path = if cfg!(windows) {
path.replace('/', "\\")
} else {
path.to_string()
};
Some(self.args.path.join(path))
Some(self.args.serve_path.join(path))
}
async fn list_dir(

56
tests/config.rs Normal file
View file

@ -0,0 +1,56 @@
mod fixtures;
mod utils;
use assert_cmd::prelude::*;
use assert_fs::TempDir;
use diqwest::blocking::WithDigestAuth;
use fixtures::{port, tmpdir, wait_for_port, Error};
use rstest::rstest;
use std::path::PathBuf;
use std::process::{Command, Stdio};
#[rstest]
fn use_config_file(tmpdir: TempDir, port: u16) -> Result<(), Error> {
let config_path = get_config_path().display().to_string();
let mut child = Command::cargo_bin("dufs")?
.arg(tmpdir.path())
.arg("-p")
.arg(port.to_string())
.args(["--config", &config_path])
.stdout(Stdio::piped())
.spawn()?;
wait_for_port(port);
let url = format!("http://localhost:{port}/dufs/index.html");
let resp = fetch!(b"GET", &url).send()?;
assert_eq!(resp.status(), 401);
let url = format!("http://localhost:{port}/dufs/index.html");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
assert_eq!(resp.text()?, "This is index.html");
let url = format!("http://localhost:{port}/dufs?simple");
let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
let text: String = resp.text().unwrap();
assert!(text.split('\n').any(|c| c == "dir1/"));
assert!(!text.split('\n').any(|c| c == "dir3/"));
assert!(!text.split('\n').any(|c| c == "test.txt"));
let url = format!("http://localhost:{port}/dufs/dir1/upload.txt");
let resp = fetch!(b"PUT", &url)
.body("Hello")
.send_with_digest_auth("user", "pass")?;
assert_eq!(resp.status(), 201);
child.kill()?;
Ok(())
}
fn get_config_path() -> PathBuf {
let mut path = std::env::current_dir().expect("Failed to get current directory");
path.push("tests");
path.push("data");
path.push("config.yaml");
path
}

9
tests/data/config.yaml Normal file
View file

@ -0,0 +1,9 @@
bind:
- 0.0.0.0
path-prefix: dufs
hidden:
- dir3
- test.txt
auth:
- user:pass@/:rw
allow-upload: true

View file

@ -41,7 +41,7 @@ fn log_remote_user(
assert_eq!(resp.status(), 200);
let mut buf = [0; 4096];
let mut buf = [0; 2048];
let buf_len = stdout.read(&mut buf)?;
let output = std::str::from_utf8(&buf[0..buf_len])?;
@ -69,7 +69,7 @@ fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error
let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?;
assert_eq!(resp.status(), 200);
let mut buf = [0; 4096];
let mut buf = [0; 2048];
let buf_len = stdout.read(&mut buf)?;
let output = std::str::from_utf8(&buf[0..buf_len])?;

View file

@ -7,6 +7,8 @@ use predicates::str::contains;
use reqwest::blocking::ClientBuilder;
use rstest::rstest;
use crate::fixtures::port;
/// Can start the server with TLS and receive encrypted responses.
#[rstest]
#[case(server(&[
@ -33,8 +35,16 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
/// Wrong path for cert throws error.
#[rstest]
fn wrong_path_cert() -> Result<(), Error> {
let port = port().to_string();
Command::cargo_bin("dufs")?
.args(["--tls-cert", "wrong", "--tls-key", "tests/data/key.pem"])
.args([
"--tls-cert",
"wrong",
"--tls-key",
"tests/data/key.pem",
"--port",
&port,
])
.assert()
.failure()
.stderr(contains("Failed to access `wrong`"));
@ -45,8 +55,16 @@ fn wrong_path_cert() -> Result<(), Error> {
/// Wrong paths for key throws errors.
#[rstest]
fn wrong_path_key() -> Result<(), Error> {
let port = port().to_string();
Command::cargo_bin("dufs")?
.args(["--tls-cert", "tests/data/cert.pem", "--tls-key", "wrong"])
.args([
"--tls-cert",
"tests/data/cert.pem",
"--tls-key",
"wrong",
"--port",
&port,
])
.assert()
.failure()
.stderr(contains("Failed to access `wrong`"));