-b, --bind <addr>... Specify bind address
-p, --port <port> Specify port to listen on [default: 5000]
--path-prefix <path> Specify an path prefix
+ --hidden <names> Comma-separated list of names to hide from directory listings
-a, --auth <rule>... Add auth for path
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
-A, --allow-all Allow all operations
--allow-symlink Allow symlink to files/folders outside root directory
--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-try-index Serve index.html when requesting a directory, returns file 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)
--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
dufs -p 80
```
+Hide folders from directory listing
+
+```
+dufs --hidden .git,.DS_Store
+```
+
Use https
```
.value_name("path")
.help("Specify an path prefix"),
)
+ .arg(
+ Arg::new("hidden")
+ .long("hidden")
+ .help("Comma-separated list of names to hide from directory listings")
+ .value_name("names"),
+ )
.arg(
Arg::new("auth")
.short('a')
.arg(
Arg::new("render-try-index")
.long("render-try-index")
- .help("Serve index.html when requesting a directory, returns file listing if not found index.html"),
+ .help("Serve index.html when requesting a directory, returns directory listing if not found index.html"),
)
.arg(
Arg::new("render-spa")
pub path_is_file: bool,
pub path_prefix: String,
pub uri_prefix: String,
+ pub hidden: String,
pub auth_method: AuthMethod,
pub auth: AccessControl,
pub allow_upload: bool,
} else {
format!("/{}/", &path_prefix)
};
+ let hidden: String = matches
+ .value_of("hidden")
+ .map(|v| format!(",{},", v))
+ .unwrap_or_default();
let enable_cors = matches.is_present("enable-cors");
let auth: Vec<&str> = matches
.values_of("auth")
path_is_file,
path_prefix,
uri_prefix,
+ hidden,
auth_method,
auth,
enable_cors,
use crate::streamer::Streamer;
-use crate::utils::{decode_uri, encode_uri};
+use crate::utils::{decode_uri, encode_uri, get_file_name, try_get_file_name};
use crate::{Args, BoxResult};
+use async_walkdir::{Filtering, WalkDir};
use xml::escape::escape_str_pcdata;
-use async_walkdir::WalkDir;
use async_zip::write::{EntryOptions, ZipFileWriter};
use async_zip::Compression;
use chrono::{TimeZone, Utc};
self.handle_zip_dir(path, head_only, &mut res).await?;
} else if allow_search && query.starts_with("q=") {
let q = decode_uri(&query[2..]).unwrap_or_default();
- self.handle_query_dir(path, &q, head_only, &mut res).await?;
+ self.handle_search_dir(path, &q, head_only, &mut res)
+ .await?;
} else {
self.handle_ls_dir(path, true, head_only, &mut res).await?;
}
self.send_index(path, paths, exist, head_only, res)
}
- async fn handle_query_dir(
+ async fn handle_search_dir(
&self,
path: &Path,
- query: &str,
+ search: &str,
head_only: bool,
res: &mut Response,
) -> BoxResult<()> {
let mut paths: Vec<PathItem> = vec![];
- let mut walkdir = WalkDir::new(path);
- while let Some(entry) = walkdir.next().await {
- if let Ok(entry) = entry {
- if !entry
- .file_name()
- .to_string_lossy()
+ let hidden = self.args.hidden.to_string();
+ let search = search.to_string();
+ let mut walkdir = WalkDir::new(path).filter(move |entry| {
+ let hidden_cloned = hidden.clone();
+ let search_cloned = search.clone();
+ async move {
+ let entry_path = entry.path();
+ let base_name = get_file_name(&entry_path);
+ if is_hidden(&hidden_cloned, base_name) {
+ return Filtering::IgnoreDir;
+ }
+ if !base_name
.to_lowercase()
- .contains(&query.to_lowercase())
+ .contains(&search_cloned.to_lowercase())
{
- continue;
+ return Filtering::Ignore;
}
if fs::symlink_metadata(entry.path()).await.is_err() {
- continue;
+ return Filtering::Ignore;
}
+ Filtering::Continue
+ }
+ });
+ while let Some(entry) = walkdir.next().await {
+ if let Ok(entry) = entry {
if let Ok(Some(item)) = self.to_pathitem(entry.path(), path.to_path_buf()).await {
paths.push(item);
}
res: &mut Response,
) -> BoxResult<()> {
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
- let filename = get_file_name(path)?;
+ let filename = try_get_file_name(path)?;
res.headers_mut().insert(
CONTENT_DISPOSITION,
HeaderValue::from_str(&format!(
return Ok(());
}
let path = path.to_owned();
+ let hidden = self.args.hidden.clone();
tokio::spawn(async move {
- if let Err(e) = zip_dir(&mut writer, &path).await {
+ if let Err(e) = zip_dir(&mut writer, &path, &hidden).await {
error!("Failed to zip {}, {}", path.display(), e);
}
});
);
}
- let filename = get_file_name(path)?;
+ let filename = try_get_file_name(path)?;
res.headers_mut().insert(
CONTENT_DISPOSITION,
HeaderValue::from_str(&format!("inline; filename=\"{}\"", encode_uri(filename),))
let mut rd = fs::read_dir(entry_path).await?;
while let Ok(Some(entry)) = rd.next_entry().await {
let entry_path = entry.path();
+ let base_name = get_file_name(&entry_path);
+ if is_hidden(&self.args.hidden, base_name) {
+ continue;
+ }
if let Ok(Some(item)) = self.to_pathitem(entry_path.as_path(), base_path).await {
paths.push(item);
}
),
}
}
- fn base_name(&self) -> &str {
- Path::new(&self.name)
- .file_name()
- .and_then(|v| v.to_str())
- .unwrap_or_default()
+ pub fn base_name(&self) -> &str {
+ self.name.split('/').last().unwrap_or_default()
}
}
));
}
-async fn zip_dir<W: AsyncWrite + Unpin>(writer: &mut W, dir: &Path) -> BoxResult<()> {
+async fn zip_dir<W: AsyncWrite + Unpin>(writer: &mut W, dir: &Path, hidden: &str) -> BoxResult<()> {
let mut writer = ZipFileWriter::new(writer);
- let mut walkdir = WalkDir::new(dir);
- while let Some(entry) = walkdir.next().await {
- if let Ok(entry) = entry {
+ let hidden = hidden.to_string();
+ let mut walkdir = WalkDir::new(dir).filter(move |entry| {
+ let hidden = hidden.clone();
+ async move {
let entry_path = entry.path();
+ let base_name = get_file_name(&entry_path);
+ if is_hidden(&hidden, base_name) {
+ return Filtering::IgnoreDir;
+ }
let meta = match fs::symlink_metadata(entry.path()).await {
Ok(meta) => meta,
- Err(_) => continue,
+ Err(_) => return Filtering::Ignore,
};
if !meta.is_file() {
- continue;
+ return Filtering::Ignore;
}
+ Filtering::Continue
+ }
+ });
+ while let Some(entry) = walkdir.next().await {
+ if let Ok(entry) = entry {
+ let entry_path = entry.path();
let filename = match entry_path.strip_prefix(dir).ok().and_then(|v| v.to_str()) {
Some(v) => v,
None => continue,
*res.status_mut() = StatusCode::NO_CONTENT;
}
-fn get_file_name(path: &Path) -> BoxResult<&str> {
- path.file_name()
- .and_then(|v| v.to_str())
- .ok_or_else(|| format!("Failed to get file name of `{}`", path.display()).into())
+fn is_hidden(hidden: &str, file_name: &str) -> bool {
+ hidden.contains(&format!(",{},", file_name))
}
fn set_webdav_headers(res: &mut Response) {
-use std::borrow::Cow;
+use crate::BoxResult;
+use std::{borrow::Cow, path::Path};
pub fn encode_uri(v: &str) -> String {
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
.decode_utf8()
.ok()
}
+
+pub fn get_file_name(path: &Path) -> &str {
+ path.file_name()
+ .and_then(|v| v.to_str())
+ .unwrap_or_default()
+}
+
+pub fn try_get_file_name(path: &Path) -> BoxResult<&str> {
+ path.file_name()
+ .and_then(|v| v.to_str())
+ .ok_or_else(|| format!("Failed to get file name of `{}`", path.display()).into())
+}
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
for p in paths {
- assert!(p.contains(&"test.html"));
+ assert!(p.contains("test.html"));
}
Ok(())
}
#[rstest]
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}{}", server.url(), "xyz"))?;
- assert_index_resp!(resp);
+ assert_resp_paths!(resp);
Ok(())
}
assert_eq!(resp.headers().get("content-type").unwrap(), "image/x-icon");
Ok(())
}
+
+#[rstest]
+fn assets_with_prefix(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
+ let ver = env!("CARGO_PKG_VERSION");
+ let resp = reqwest::blocking::get(format!("{}xyz/", server.url()))?;
+ let index_js = format!("/xyz/__dufs_v{}_index.js", ver);
+ let index_css = format!("/xyz/__dufs_v{}_index.css", ver);
+ let favicon_ico = format!("/xyz/__dufs_v{}_favicon.ico", ver);
+ let text = resp.text()?;
+ assert!(text.contains(&format!(r#"href="{}""#, index_css)));
+ assert!(text.contains(&format!(r#"href="{}""#, favicon_ico)));
+ assert!(text.contains(&format!(r#"src="{}""#, index_js)));
+ Ok(())
+}
+
+#[rstest]
+fn asset_js_with_prefix(
+ #[with(&["--path-prefix", "xyz"])] server: TestServer,
+) -> Result<(), Error> {
+ let url = format!(
+ "{}xyz/__dufs_v{}_index.js",
+ server.url(),
+ env!("CARGO_PKG_VERSION")
+ );
+ let resp = reqwest::blocking::get(url)?;
+ assert_eq!(resp.status(), 200);
+ assert_eq!(
+ resp.headers().get("content-type").unwrap(),
+ "application/javascript"
+ );
+ Ok(())
+}
#[allow(dead_code)]
pub static DIR_NO_INDEX: &str = "dir-no-index/";
+/// Directory names for testing hidden
+#[allow(dead_code)]
+pub static DIR_GIT: &str = ".git/";
+
/// Directory names for testing purpose
#[allow(dead_code)]
-pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX];
+pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX, DIR_GIT];
/// Test fixture which creates a temporary directory with a few files and directories inside.
/// The directories also contain files.
--- /dev/null
+mod fixtures;
+mod utils;
+
+use fixtures::{server, Error, TestServer};
+use rstest::rstest;
+
+#[rstest]
+#[case(server(&[] as &[&str]), true)]
+#[case(server(&["--hidden", ".git,index.html"]), false)]
+fn hidden_get_dir(#[case] server: TestServer, #[case] exist: bool) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(server.url())?;
+ assert_eq!(resp.status(), 200);
+ let paths = utils::retrive_index_paths(&resp.text()?);
+ assert_eq!(paths.contains(".git/"), exist);
+ assert_eq!(paths.contains("index.html"), exist);
+ Ok(())
+}
+
+#[rstest]
+#[case(server(&[] as &[&str]), true)]
+#[case(server(&["--hidden", ".git,index.html"]), false)]
+fn hidden_propfind_dir(#[case] server: TestServer, #[case] exist: bool) -> Result<(), Error> {
+ let resp = fetch!(b"PROPFIND", server.url()).send()?;
+ assert_eq!(resp.status(), 207);
+ let body = resp.text()?;
+ assert_eq!(body.contains("<D:href>/.git/</D:href>"), exist);
+ assert_eq!(body.contains("<D:href>/index.html</D:href>"), exist);
+ Ok(())
+}
+
+#[rstest]
+#[case(server(&["--allow-search"] as &[&str]), true)]
+#[case(server(&["--allow-search", "--hidden", ".git,test.html"]), false)]
+fn hidden_search_dir(#[case] server: TestServer, #[case] exist: bool) -> Result<(), Error> {
+ let resp = reqwest::blocking::get(format!("{}?q={}", server.url(), "test.html"))?;
+ assert_eq!(resp.status(), 200);
+ let paths = utils::retrive_index_paths(&resp.text()?);
+ for p in paths {
+ assert_eq!(p.contains("test.html"), exist);
+ }
+ Ok(())
+}
#[rstest]
fn get_dir(server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
- assert_index_resp!(resp);
+ assert_resp_paths!(resp);
Ok(())
}
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
for p in paths {
- assert!(p.contains(&"test.html"));
+ assert!(p.contains("test.html"));
}
Ok(())
}
let paths = utils::retrive_index_paths(&resp.text()?);
assert!(!paths.is_empty());
for p in paths {
- assert!(p.contains(&"😀.bin"));
+ assert!(p.contains("😀.bin"));
}
Ok(())
}
mod fixtures;
mod utils;
-use fixtures::{server, Error, TestServer, DIR_NO_FOUND, DIR_NO_INDEX};
+use fixtures::{server, Error, TestServer, DIR_NO_FOUND, DIR_NO_INDEX, FILES};
use rstest::rstest;
#[rstest]
#[rstest]
fn render_try_index2(#[with(&["--render-try-index"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}{}", server.url(), DIR_NO_INDEX))?;
- let files: Vec<&str> = self::fixtures::FILES
+ let files: Vec<&str> = FILES
.iter()
.filter(|v| **v != "index.html")
.cloned()
.collect();
- assert_index_resp!(resp, files);
+ assert_resp_paths!(resp, files);
Ok(())
}
.danger_accept_invalid_certs(true)
.build()?;
let resp = client.get(server.url()).send()?.error_for_status()?;
- assert_index_resp!(resp);
+ assert_resp_paths!(resp);
Ok(())
}
use std::collections::HashSet;
#[macro_export]
-macro_rules! assert_index_resp {
+macro_rules! assert_resp_paths {
($resp:ident) => {
- assert_index_resp!($resp, self::fixtures::FILES)
+ assert_resp_paths!($resp, self::fixtures::FILES)
};
($resp:ident, $files:expr) => {
assert_eq!($resp.status(), 200);