index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2022-09-12 20:51:32.0 +00:00:00
committer GitHub <noreply@github.com> 2022-09-12 20:51:32.0 +00:00:00
commit
91b6ba2cf04fd333de5e087969817eb64f56b844 [patch]
tree
7061de11f37e3e851c6bdc2188101449cacab049
parent
467affc2b4a7083e7ebfb8aa6bbbadce61f902a8
download
91b6ba2cf04fd333de5e087969817eb64f56b844.tar.gz

Relative paths for directory listing entries (#137)

resolves #136

* feat: relative paths for directory listing entries
* docs: relative paths for entries info

Diff

 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.

<img title="SWS - Directory Listing" src="https://user-images.githubusercontent.com/1700322/145420578-5a508d2a-773b-4239-acc0-197ea2062ff4.png" width="400">

## 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<Response<Body>> {
    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<String>)> = 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!(
            "{}<tr><td><a href=\"{}\" title=\"{}\">{}</a></td><td>{}</td><td align=\"right\">{}</td></tr>",
            entries_str,
            uri,
            name,
            name,
            modified,
            filesize_str
            "{}<tr><td><a href=\"{}\">{}</a></td><td>{}</td><td align=\"right\">{}</td></tr>",
            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<P: AsRef<Path>>(dir: P) -> PathBuf
    where
        PathBuf: From<P>,
    {
        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);
                }
            }
        }
    }
}