]> OzVa Git service - ozva-cloud/commitdiff
feat: support hiding folders with --hidden (#73)
authorsigoden <sigoden@gmail.com>
Sat, 25 Jun 2022 00:15:16 +0000 (08:15 +0800)
committerGitHub <noreply@github.com>
Sat, 25 Jun 2022 00:15:16 +0000 (08:15 +0800)
13 files changed:
README.md
src/args.rs
src/server.rs
src/utils.rs
tests/allow.rs
tests/args.rs
tests/assets.rs
tests/fixtures.rs
tests/hidden.rs [new file with mode: 0644]
tests/http.rs
tests/render.rs
tests/tls.rs
tests/utils.rs

index 04798e1db83935ac4d43c85c6e4e9a86cfa6ee12..52bb3d5add928a4ea08bbc1b8c9c1a28f5b11d53 100644 (file)
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ OPTIONS:
     -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
@@ -61,7 +62,7 @@ OPTIONS:
         --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
@@ -125,6 +126,12 @@ Listen on a specific port
 dufs -p 80
 ```
 
+Hide folders from directory listing
+
+```
+dufs --hidden .git,.DS_Store
+```
+
 Use https
 
 ```
index 425311202b3b7699b267572e77491990feae5d32..a91e00b3a3ba95519cbfb9da6bb48acc483f7cdd 100644 (file)
@@ -48,6 +48,12 @@ fn app() -> Command<'static> {
                 .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')
@@ -104,7 +110,7 @@ fn app() -> Command<'static> {
         .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")
@@ -137,6 +143,7 @@ pub struct Args {
     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,
@@ -173,6 +180,10 @@ impl Args {
         } 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")
@@ -206,6 +217,7 @@ impl Args {
             path_is_file,
             path_prefix,
             uri_prefix,
+            hidden,
             auth_method,
             auth,
             enable_cors,
index 6c8f38404717fb4697c1641497d872d093f4c218..7c580764d9cf97997475ce6621a8a2cf17ebd1c2 100644 (file)
@@ -1,9 +1,9 @@
 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};
@@ -162,7 +162,8 @@ impl Server {
                         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?;
                     }
@@ -322,28 +323,39 @@ impl Server {
         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);
                 }
@@ -359,7 +371,7 @@ impl Server {
         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!(
@@ -374,8 +386,9 @@ impl Server {
             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);
             }
         });
@@ -513,7 +526,7 @@ impl Server {
             );
         }
 
-        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),))
@@ -802,6 +815,10 @@ DATA = {}
         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);
             }
@@ -910,11 +927,8 @@ impl PathItem {
             ),
         }
     }
-    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()
     }
 }
 
@@ -978,19 +992,30 @@ fn res_multistatus(res: &mut Response, content: &str) {
     ));
 }
 
-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,
@@ -1061,10 +1086,8 @@ fn status_no_content(res: &mut Response) {
     *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) {
index ac2c8fe8f826619a8b045caa3500151f3bf257c6..6a27b65641d9663c812af6d77cfd1270d66c0277 100644 (file)
@@ -1,4 +1,5 @@
-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();
@@ -10,3 +11,15 @@ pub fn decode_uri(v: &str) -> Option<Cow<str>> {
         .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())
+}
index 6a1f4cdb922982717b0f71940f2913b291634b1e..f83a8914c4c00d2d6ad47bc9c6f1f9e6e355e1cd 100644 (file)
@@ -67,7 +67,7 @@ fn allow_search(#[with(&["--allow-search"])] server: TestServer) -> Result<(), E
     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(())
 }
index f2bec6334f6994fd527fa679905ccf83ca4aea25..83086e965d0b1ca890fa721619b55b8b0d755578 100644 (file)
@@ -10,7 +10,7 @@ use std::process::{Command, Stdio};
 #[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(())
 }
 
index b5a1e9518139dd70068f7db257f37ac806f80511..ecc566262bc2307da5434d10c6a8ae4a70ea5583 100644 (file)
@@ -59,3 +59,35 @@ fn asset_ico(server: TestServer) -> Result<(), Error> {
     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(())
+}
index f97276b03db09783d0bed3968335ea23a55b073a..f53457fb099f8719efde5a4445a7cd151b45e7ea 100644 (file)
@@ -23,9 +23,13 @@ pub static DIR_NO_FOUND: &str = "dir-no-found/";
 #[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.
diff --git a/tests/hidden.rs b/tests/hidden.rs
new file mode 100644 (file)
index 0000000..1f4f91f
--- /dev/null
@@ -0,0 +1,42 @@
+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(())
+}
index daa8e22cf49025751cc7c6aab1a87e60f39ac563..bb52356a76396fedf7c2e5bf90dd7a528744d9bc 100644 (file)
@@ -7,7 +7,7 @@ use rstest::rstest;
 #[rstest]
 fn get_dir(server: TestServer) -> Result<(), Error> {
     let resp = reqwest::blocking::get(server.url())?;
-    assert_index_resp!(resp);
+    assert_resp_paths!(resp);
     Ok(())
 }
 
@@ -69,7 +69,7 @@ fn get_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
     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(())
 }
@@ -81,7 +81,7 @@ fn get_dir_search2(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
     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(())
 }
index 9611113f169cd439dec6c9eb3cbc60be97e2f90d..9ecfd8e36525ca55f749b27529bfb80f9c743ffa 100644 (file)
@@ -1,7 +1,7 @@
 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]
@@ -30,12 +30,12 @@ fn render_try_index(#[with(&["--render-try-index"])] server: TestServer) -> Resu
 #[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(())
 }
 
index 94c7ab876636ec10ef1710264961d93c0afff2d8..ca4c65cc4aea74f8a5b3906908fecaa1c7b505e1 100644 (file)
@@ -22,7 +22,7 @@ fn tls_works(#[case] server: TestServer) -> Result<(), Error> {
         .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(())
 }
 
index d33a4731ce6fc0be8ecf9028535946210813ef96..a0cff6e2386f8710e8257692fc62ba787f9bba94 100644 (file)
@@ -2,9 +2,9 @@ use serde_json::Value;
 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);