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
This commit is contained in:
sigoden 2023-02-20 11:05:53 +08:00 committed by GitHub
parent c6dcaf95d4
commit 7d6d7d49ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 43 deletions

View file

@ -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`
```
<details>
<summary><h2>Advanced topics</h2></summary>

View file

@ -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::<Vec<String>>()
.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<PathItem>,
allow_upload: bool,
allow_delete: bool,
allow_search: bool,
allow_archive: bool,
dir_exists: bool,
paths: Vec<PathItem>,
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]

View file

@ -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",

View file

@ -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}");
}
}
}

View file

@ -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()?;

View file

@ -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];

View file

@ -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()?;

View file

@ -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);

View file

@ -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(())
}

View file

@ -48,7 +48,7 @@ fn retrieve_index_paths_impl(index: &str) -> Option<IndexSet<String>> {
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())
}