From 91b6ba2cf04fd333de5e087969817eb64f56b844 Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Mon, 12 Sep 2022 22:51:32 +0200 Subject: [PATCH] Relative paths for directory listing entries (#137) resolves #136 * feat: relative paths for directory listing entries * docs: relative paths for entries info --- docs/content/features/directory-listing.md | 11 ++++++++++- src/static_files.rs | 48 ++++++++++++++++++++++++++++++++++++------------ tests/dir_listing.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 167 insertions(+), 29 deletions(-) diff --git a/docs/content/features/directory-listing.md b/docs/content/features/directory-listing.md index 76635ce..9c174c7 100644 --- a/docs/content/features/directory-listing.md +++ b/docs/content/features/directory-listing.md @@ -11,10 +11,19 @@ static-web-server \ --directory-listing true ``` -And here an example of how the directory listing looks like. +And here is an example of how the directory listing looks like. +## Relative paths for entries + +SWS uses relative paths for the directory listing entries (file or directory) and is used regardless of the [redirect trailing slash](../features/trailing-slash-redirect.md) feature. + +However, when the *"redirect trailing slash"* feature is disabled and a directory request URI doesn't contain a trailing slash then the entries will contain the path `parent-dir/entry-name` as the link value. Otherwise, just an `entry-name` link value is used (default behavior). + +Note also that in both cases, SWS will append a trailing slash to the entry if is a directory. + + ## Sorting Sorting by `Name`, `Last modified` and `Size` is enabled as clickable columns when the directory listing is activated via the `--directory-listing=true` option. diff --git a/src/static_files.rs b/src/static_files.rs index f612b7b..f0cfae7 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -145,8 +145,8 @@ fn path_from_tail( } /// Provides directory listing support for the current request. -/// Note that this function is a highly dependent on `path_from_tail()` -// function which must be called first. See `handle()` more for details. +/// Note that this function highly depends on `path_from_tail()` function +/// which must be called first. See `handle()` for more details. fn directory_listing<'a>( method: &'a Method, current_path: &'a str, @@ -209,7 +209,8 @@ fn directory_listing<'a>( }) } -// It reads current directory entries and create the index page content. Otherwise returns a status error. +/// It reads the current directory entries and create an index page content. +/// Otherwise it returns a status error. async fn read_directory_entries( mut entries: tokio::fs::ReadDir, base_path: &str, @@ -219,7 +220,7 @@ async fn read_directory_entries( ) -> Result> { let mut dirs_count: usize = 0; let mut files_count: usize = 0; - let mut files_found: Vec<(String, String, u64, String)> = Vec::new(); + let mut files_found: Vec<(String, String, u64, Option)> = vec![]; while let Some(entry) = entries.next_entry().await? { let meta = entry.metadata().await?; @@ -250,7 +251,33 @@ async fn read_directory_entries( continue; } - let uri = [base_path, &name].concat(); + let mut uri = None; + // NOTE: Use relative paths by default independently of + // the "redirect trailing slash" feature. + // However, when "redirect trailing slash" is disabled + // and a request path doesn't contain a trailing slash then + // entries should contain the "parent/entry-name" as a link format. + // Otherwise, we just use the "entry-name" as a link (default behavior). + // Note that in both cases, we add a trailing slash if the entry is a directory. + if !base_path.ends_with('/') { + let base_path = Path::new(base_path); + let parent_dir = base_path.parent().unwrap_or(base_path); + let mut base_dir = base_path; + if base_path != parent_dir { + base_dir = base_path.strip_prefix(parent_dir)?; + } + let mut base_str = String::new(); + if !base_dir.starts_with("/") { + let base_dir = base_dir.to_str().unwrap_or_default(); + if !base_dir.is_empty() { + base_str.push_str(base_dir); + } + base_str.push('/'); + } + base_str.push_str(&name); + uri = Some(base_str); + } + let modified = match parse_last_modified(meta.modified()?) { Ok(tm) => tm.to_local().strftime("%F %T")?.to_string(), Err(err) => { @@ -340,14 +367,11 @@ async fn read_directory_entries( filesize_str = String::from("-"); } + let entry_uri = uri.unwrap_or_else(|| name.to_owned()); + entries_str = format!( - "{}{}{}{}", - entries_str, - uri, - name, - name, - modified, - filesize_str + "{}{}{}{}", + entries_str, entry_uri, name, modified, filesize_str ); } diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs index f0430d3..39db511 100644 --- a/tests/dir_listing.rs +++ b/tests/dir_listing.rs @@ -7,31 +7,35 @@ mod tests { use headers::HeaderMap; use http::{Method, StatusCode}; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; use static_web_server::static_files::{self, HandleOpts}; - fn root_dir() -> PathBuf { - PathBuf::from("docker/public/") + const METHODS: [Method; 8] = [ + Method::CONNECT, + Method::DELETE, + Method::GET, + Method::HEAD, + Method::PATCH, + Method::POST, + Method::PUT, + Method::TRACE, + ]; + + fn root_dir>(dir: P) -> PathBuf + where + PathBuf: From

, + { + PathBuf::from(dir) } #[tokio::test] - async fn dir_listing_redirect_permanent_uri() { - let methods = [ - Method::CONNECT, - Method::DELETE, - Method::GET, - Method::HEAD, - Method::PATCH, - Method::POST, - Method::PUT, - Method::TRACE, - ]; - for method in methods { + async fn dir_listing_redirect_trailing_slash_dir() { + for method in METHODS { match static_files::handle(&HandleOpts { method: &method, headers: &HeaderMap::new(), - base_path: &root_dir(), + base_path: &root_dir("docker/public/"), uri_path: "/assets", uri_query: None, dir_listing: true, @@ -51,4 +55,105 @@ mod tests { } } } + + #[tokio::test] + async fn dir_listing_redirect_trailing_slash_relative_dir_path() { + for method in METHODS { + match static_files::handle(&HandleOpts { + method: &method, + headers: &HeaderMap::new(), + base_path: &root_dir("docs/"), + uri_path: "/content/", + uri_query: None, + dir_listing: true, + dir_listing_order: 6, + redirect_trailing_slash: true, + }) + .await + { + Ok(mut res) => { + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8"); + + let body = hyper::body::to_bytes(res.body_mut()) + .await + .expect("unexpected bytes error during `body` conversion"); + let body_str = std::str::from_utf8(&body).unwrap(); + // directory link should only contain "dir-name/" in a relative way + assert_eq!( + body_str.contains(r#"href="features/""#), + method == Method::GET + ); + } + Err(status) => { + assert!(method != Method::GET && method != Method::HEAD); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } + } + } + + #[tokio::test] + async fn dir_listing_no_redirect_trailing_slash_relative_dir_path() { + for method in METHODS { + match static_files::handle(&HandleOpts { + method: &method, + headers: &HeaderMap::new(), + base_path: &root_dir("docs/"), + uri_path: "/content", + uri_query: None, + dir_listing: true, + dir_listing_order: 6, + redirect_trailing_slash: false, + }) + .await + { + Ok(mut res) => { + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8"); + + let body = hyper::body::to_bytes(res.body_mut()) + .await + .expect("unexpected bytes error during `body` conversion"); + let body_str = std::str::from_utf8(&body).unwrap(); + // directory link should contain "parent/dir-name/" in a relative way + assert_eq!( + body_str.contains(r#"href="content/features/""#), + method == Method::GET + ); + } + Err(status) => { + assert!(method != Method::GET && method != Method::HEAD); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } + } + } + + #[tokio::test] + async fn dir_listing_no_redirect_trailing_slash_relative_file_path() { + for method in METHODS { + match static_files::handle(&HandleOpts { + method: &method, + headers: &HeaderMap::new(), + base_path: &root_dir("docs/"), + uri_path: "/README.md", + uri_query: None, + dir_listing: true, + dir_listing_order: 6, + redirect_trailing_slash: false, + }) + .await + { + Ok(res) => { + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-type"], "text/markdown"); + } + Err(status) => { + assert!(method != Method::GET && method != Method::HEAD); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } + } + } } -- libgit2 1.7.2