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(-)
@@ -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.
@@ -145,8 +145,8 @@ fn path_from_tail(
}
fn directory_listing<'a>(
method: &'a Method,
current_path: &'a str,
@@ -209,7 +209,8 @@ fn directory_listing<'a>(
})
}
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;
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
);
}
@@ -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();
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();
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);
}
}
}
}
}