"rustls-pemfile",
"serde",
"serde_json",
+ "serde_yaml",
"socket2 0.5.3",
"tokio",
"tokio-rustls",
[[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",
"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"
"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"
chardetng = "0.1"
glob = "0.3.1"
indexmap = "2.0"
+serde_yaml = "0.9.27"
[features]
default = ["tls"]
```
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
--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
```
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
--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
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 {
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")
.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(
.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(
.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")]
.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,
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 {
/// 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())
}
- _ => 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,
- })
- }
+ if let Some(tls_key) = matches.get_one::<PathBuf>("tls-key") {
+ args.tls_key = Some(tls_key.clone())
+ }
- fn parse_addrs(addrs: &[&str]) -> Result<Vec<BindAddr>> {
- let mut bind_addrs = vec![];
- let mut invalid_addrs = vec![];
- for addr in addrs {
- match addr.parse::<IpAddr>() {
- Ok(v) => {
- bind_addrs.push(BindAddr::Address(v));
- }
- Err(_) => {
- if cfg!(unix) {
- bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
- } else {
- invalid_addrs.push(*addr);
- }
- }
+ match (&args.tls_cert, &args.tls_key) {
+ (Some(_), Some(_)) => {}
+ (Some(_), _) => bail!("No tls-key set"),
+ (_, Some(_)) => bail!("No tls-cert set"),
+ (None, None) => {}
}
}
- if !invalid_addrs.is_empty() {
- bail!("Invalid bind address `{}`", invalid_addrs.join(","));
+ #[cfg(not(feature = "tls"))]
+ {
+ args.tls_cert = None;
+ args.tls_key = None;
}
- Ok(bind_addrs)
+
+ Ok(args)
}
- fn parse_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
+ 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());
.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)?;
+ 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());
}
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 {
+ match addr.parse::<IpAddr>() {
+ Ok(v) => {
+ bind_addrs.push(BindAddr::Address(v));
+ }
+ Err(_) => {
+ if cfg!(unix) {
+ bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
+ } else {
+ invalid_addrs.push(*addr);
+ }
+ }
+ }
+ }
+ if !invalid_addrs.is_empty() {
+ bail!("Invalid bind address `{}`", invalid_addrs.join(","));
+ }
+ Ok(bind_addrs)
+ }
+}
+
+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(".")
+}
};
}
-#[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![];
elements: Vec<LogElement>,
}
+impl Default for LogHttp {
+ fn default() -> Self {
+ DEFAULT_LOG_FORMAT.parse().unwrap()
+ }
+}
+
#[derive(Debug)]
enum LogElement {
Variable(String),
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};
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()
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))
tokio::spawn(hyper::Server::builder(incoming).serve(new_service));
handles.push(server);
}
+ _ => {
+ unreachable!()
+ }
};
}
BindAddr::Path(path) => {
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(),
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),
};
.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);
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 {
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?;
) -> 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();
},
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);
}
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);
}
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,
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()
}
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(
--- /dev/null
+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
+}
--- /dev/null
+bind:
+ - 0.0.0.0
+path-prefix: dufs
+hidden:
+ - dir3
+ - test.txt
+auth:
+ - user:pass@/:rw
+allow-upload: true
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])?;
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])?;
use reqwest::blocking::ClientBuilder;
use rstest::rstest;
+use crate::fixtures::port;
+
/// Can start the server with TLS and receive encrypted responses.
#[rstest]
#[case(server(&[
/// 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`"));
/// 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`"));