From 7d6d7d49ca0b31226a72b8f84501205449b02388 Mon Sep 17 00:00:00 2001 From: sigoden Date: Mon, 20 Feb 2023 11:05:53 +0800 Subject: [PATCH] feat: API to search and list directories (#177) use `?simple` to output path name only. use `?json` to output paths in json format. By default, output html page. close #166 --- README.md | 10 ++++++++++ src/server.rs | 44 +++++++++++++++++++++++++++++++++++--------- tests/assets.rs | 26 +++++++++++++------------- tests/fixtures.rs | 10 +++++----- tests/http.rs | 36 ++++++++++++++++++++++++++++++++++++ tests/log_http.rs | 4 ++-- tests/single_file.rs | 14 +++++++------- tests/sort.rs | 8 ++++---- tests/symlink.rs | 4 ++-- tests/utils.rs | 2 +- 10 files changed, 115 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 276565e..1006499 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,16 @@ Delete a file/folder curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder ``` +List/search directory contents + +``` +curl http://127.0.0.1:5000?simple # output pathname only, just like `ls -1` + +curl http://127.0.0.1:5000?json # output name/mtime/type/size and other information in json format + +curl http://127.0.0.1:5000?q=Dockerfile&simple # search for files, just like `find -name Dockerfile` +``` +

Advanced topics

diff --git a/src/server.rs b/src/server.rs index 5045d79..07b570f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -831,24 +831,50 @@ impl Server { } else { paths.sort_unstable(); } + if query_params.contains_key("simple") { + let output = paths + .into_iter() + .map(|v| { + if v.is_dir() { + format!("{}/\n", v.name) + } else { + format!("{}\n", v.name) + } + }) + .collect::>() + .join(""); + res.headers_mut() + .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8)); + res.headers_mut() + .typed_insert(ContentLength(output.as_bytes().len() as u64)); + *res.body_mut() = output.into(); + if head_only { + return Ok(()); + } + return Ok(()); + } let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?)); let data = IndexData { href, uri_prefix: self.args.uri_prefix.clone(), - paths, allow_upload: self.args.allow_upload, allow_delete: self.args.allow_delete, allow_search: self.args.allow_search, allow_archive: self.args.allow_archive, dir_exists: exist, + paths, + }; + let output = if query_params.contains_key("json") { + res.headers_mut() + .typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON)); + serde_json::to_string_pretty(&data).unwrap() + } else { + res.headers_mut() + .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8)); + self.html + .replace("__ASSERTS_PREFIX__", &self.assets_prefix) + .replace("__INDEX_DATA__", &serde_json::to_string(&data).unwrap()) }; - let data = serde_json::to_string(&data).unwrap(); - let output = self - .html - .replace("__ASSERTS_PREFIX__", &self.assets_prefix) - .replace("__INDEX_DATA__", &data); - res.headers_mut() - .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8)); res.headers_mut() .typed_insert(ContentLength(output.as_bytes().len() as u64)); if head_only { @@ -996,12 +1022,12 @@ impl Server { struct IndexData { href: String, uri_prefix: String, - paths: Vec, allow_upload: bool, allow_delete: bool, allow_search: bool, allow_archive: bool, dir_exists: bool, + paths: Vec, } #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)] diff --git a/tests/assets.rs b/tests/assets.rs index aa55f21..27f1361 100644 --- a/tests/assets.rs +++ b/tests/assets.rs @@ -11,13 +11,13 @@ use std::process::{Command, Stdio}; fn assets(server: TestServer) -> Result<(), Error> { let ver = env!("CARGO_PKG_VERSION"); let resp = reqwest::blocking::get(server.url())?; - let index_js = format!("/__dufs_v{}_index.js", ver); - let index_css = format!("/__dufs_v{}_index.css", ver); - let favicon_ico = format!("/__dufs_v{}_favicon.ico", ver); + let index_js = format!("/__dufs_v{ver}_index.js"); + let index_css = format!("/__dufs_v{ver}_index.css"); + let favicon_ico = format!("/__dufs_v{ver}_favicon.ico"); 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))); + 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(()) } @@ -67,13 +67,13 @@ fn asset_ico(server: TestServer) -> Result<(), Error> { 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 index_js = format!("/xyz/__dufs_v{ver}_index.js"); + let index_css = format!("/xyz/__dufs_v{ver}_index.css"); + let favicon_ico = format!("/xyz/__dufs_v{ver}_favicon.ico"); 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))); + 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(()) } @@ -108,7 +108,7 @@ fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> { wait_for_port(port); - let url = format!("http://localhost:{}", port); + let url = format!("http://localhost:{port}"); let resp = reqwest::blocking::get(&url)?; assert!(resp.text()?.starts_with(&format!( "/__dufs_v{}_index.js;DATA", diff --git a/tests/fixtures.rs b/tests/fixtures.rs index 02f2182..78c3c96 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -44,7 +44,7 @@ pub fn tmpdir() -> TempDir { for file in FILES { tmpdir .child(file) - .write_str(&format!("This is {}", file)) + .write_str(&format!("This is {file}")) .expect("Couldn't write to file"); } for directory in DIRECTORIES { @@ -59,8 +59,8 @@ pub fn tmpdir() -> TempDir { continue; } tmpdir - .child(format!("{}{}", directory, file)) - .write_str(&format!("This is {}{}", directory, file)) + .child(format!("{directory}{file}")) + .write_str(&format!("This is {directory}{file}")) .expect("Couldn't write to file"); } } @@ -109,11 +109,11 @@ where pub fn wait_for_port(port: u16) { let start_wait = Instant::now(); - while !port_check::is_port_reachable(format!("localhost:{}", port)) { + while !port_check::is_port_reachable(format!("localhost:{port}")) { sleep(Duration::from_millis(100)); if start_wait.elapsed().as_secs() > 1 { - panic!("timeout waiting for port {}", port); + panic!("timeout waiting for port {port}"); } } } diff --git a/tests/http.rs b/tests/http.rs index 6ade44a..a7c2979 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -3,6 +3,7 @@ mod utils; use fixtures::{server, Error, TestServer}; use rstest::rstest; +use serde_json::Value; #[rstest] fn get_dir(server: TestServer) -> Result<(), Error> { @@ -49,6 +50,32 @@ fn get_dir_zip(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { Ok(()) } +#[rstest] +fn get_dir_json(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { + let resp = reqwest::blocking::get(format!("{}?json", server.url()))?; + assert_eq!(resp.status(), 200); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "application/json" + ); + let json: Value = serde_json::from_str(&resp.text().unwrap()).unwrap(); + assert!(json["paths"].as_array().is_some()); + Ok(()) +} + +#[rstest] +fn get_dir_simple(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { + let resp = reqwest::blocking::get(format!("{}?simple", server.url()))?; + assert_eq!(resp.status(), 200); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/html; charset=utf-8" + ); + let text = resp.text().unwrap(); + assert!(text.split('\n').any(|v| v == "index.html")); + Ok(()) +} + #[rstest] fn head_dir_zip(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { let resp = fetch!(b"HEAD", format!("{}?zip", server.url())).send()?; @@ -86,6 +113,15 @@ fn get_dir_search2(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { Ok(()) } +#[rstest] +fn get_dir_search3(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { + let resp = reqwest::blocking::get(format!("{}?q={}&simple", server.url(), "test.html"))?; + assert_eq!(resp.status(), 200); + let text = resp.text().unwrap(); + assert!(text.split('\n').any(|v| v == "test.html")); + Ok(()) +} + #[rstest] fn head_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?; diff --git a/tests/log_http.rs b/tests/log_http.rs index 5989138..143608e 100644 --- a/tests/log_http.rs +++ b/tests/log_http.rs @@ -31,7 +31,7 @@ fn log_remote_user( let stdout = child.stdout.as_mut().expect("Failed to get stdout"); - let req = fetch!(b"GET", &format!("http://localhost:{}", port)); + let req = fetch!(b"GET", &format!("http://localhost:{port}")); let resp = if is_basic { req.basic_auth("user", Some("pass")).send()? @@ -66,7 +66,7 @@ fn no_log(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> Result<(), Error let stdout = child.stdout.as_mut().expect("Failed to get stdout"); - let resp = fetch!(b"GET", &format!("http://localhost:{}", port)).send()?; + let resp = fetch!(b"GET", &format!("http://localhost:{port}")).send()?; assert_eq!(resp.status(), 200); let mut buf = [0; 1000]; diff --git a/tests/single_file.rs b/tests/single_file.rs index 1d97f3f..f2b3f8d 100644 --- a/tests/single_file.rs +++ b/tests/single_file.rs @@ -21,11 +21,11 @@ fn single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Err wait_for_port(port); - let resp = reqwest::blocking::get(format!("http://localhost:{}", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?; assert_eq!(resp.text()?, "This is index.html"); - let resp = reqwest::blocking::get(format!("http://localhost:{}/", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}/"))?; assert_eq!(resp.text()?, "This is index.html"); - let resp = reqwest::blocking::get(format!("http://localhost:{}/index.html", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}/index.html"))?; assert_eq!(resp.text()?, "This is index.html"); child.kill()?; @@ -46,13 +46,13 @@ fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Re wait_for_port(port); - let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz"))?; assert_eq!(resp.text()?, "This is index.html"); - let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz/", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/"))?; assert_eq!(resp.text()?, "This is index.html"); - let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz/index.html", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}/xyz/index.html"))?; assert_eq!(resp.text()?, "This is index.html"); - let resp = reqwest::blocking::get(format!("http://localhost:{}", port))?; + let resp = reqwest::blocking::get(format!("http://localhost:{port}"))?; assert_eq!(resp.status(), 404); child.kill()?; diff --git a/tests/sort.rs b/tests/sort.rs index 4563a51..3254ed3 100644 --- a/tests/sort.rs +++ b/tests/sort.rs @@ -7,9 +7,9 @@ use rstest::rstest; #[rstest] fn ls_dir_sort_by_name(server: TestServer) -> Result<(), Error> { let url = server.url(); - let resp = reqwest::blocking::get(format!("{}?sort=name&order=asc", url))?; + let resp = reqwest::blocking::get(format!("{url}?sort=name&order=asc"))?; let paths1 = self::utils::retrieve_index_paths(&resp.text()?); - let resp = reqwest::blocking::get(format!("{}?sort=name&order=desc", url))?; + let resp = reqwest::blocking::get(format!("{url}?sort=name&order=desc"))?; let mut paths2 = self::utils::retrieve_index_paths(&resp.text()?); paths2.reverse(); assert_eq!(paths1, paths2); @@ -19,9 +19,9 @@ fn ls_dir_sort_by_name(server: TestServer) -> Result<(), Error> { #[rstest] fn search_dir_sort_by_name(server: TestServer) -> Result<(), Error> { let url = server.url(); - let resp = reqwest::blocking::get(format!("{}?q={}&sort=name&order=asc", url, "test.html"))?; + let resp = reqwest::blocking::get(format!("{url}?q=test.html&sort=name&order=asc"))?; let paths1 = self::utils::retrieve_index_paths(&resp.text()?); - let resp = reqwest::blocking::get(format!("{}?q={}&sort=name&order=desc", url, "test.html"))?; + let resp = reqwest::blocking::get(format!("{url}?q=test.html&sort=name&order=desc"))?; let mut paths2 = self::utils::retrieve_index_paths(&resp.text()?); paths2.reverse(); assert_eq!(paths1, paths2); diff --git a/tests/symlink.rs b/tests/symlink.rs index 7eeeadd..2ecce06 100644 --- a/tests/symlink.rs +++ b/tests/symlink.rs @@ -22,7 +22,7 @@ fn default_not_allow_symlink(server: TestServer, tmpdir: TempDir) -> Result<(), let resp = reqwest::blocking::get(server.url())?; let paths = utils::retrieve_index_paths(&resp.text()?); assert!(!paths.is_empty()); - assert!(!paths.contains(&format!("{}/", dir))); + assert!(!paths.contains(&format!("{dir}/"))); Ok(()) } @@ -41,6 +41,6 @@ fn allow_symlink( let resp = reqwest::blocking::get(server.url())?; let paths = utils::retrieve_index_paths(&resp.text()?); assert!(!paths.is_empty()); - assert!(paths.contains(&format!("{}/", dir))); + assert!(paths.contains(&format!("{dir}/"))); Ok(()) } diff --git a/tests/utils.rs b/tests/utils.rs index 0789d33..f8d4b5a 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -48,7 +48,7 @@ fn retrieve_index_paths_impl(index: &str) -> Option> { let name = v.get("name")?.as_str()?; let path_type = v.get("path_type")?.as_str()?; if path_type.ends_with("Dir") { - Some(format!("{}/", name)) + Some(format!("{name}/")) } else { Some(name.to_owned()) }