From 997e49342478ffde2c96570f76c4fc8b458ed62a Mon Sep 17 00:00:00 2001 From: Jose Quintana <1700322+joseluisq@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:27:16 +0200 Subject: [PATCH] feat: directory listing format support (#151) option: `--directory-listing-format` formats supported: `html`, `json` default: `html` resolves #128 --- .github/workflows/devel.yml | 2 +- Cargo.lock | 18 ++++++++++++++++++ Cargo.toml | 1 + src/directory_listing.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------- src/handler.rs | 8 ++++++-- src/server.rs | 5 +++++ src/settings/cli.rs | 13 +++++++++++++ src/settings/file.rs | 2 ++ src/settings/mod.rs | 5 +++++ src/static_files.rs | 3 +++ tests/compression_static.rs | 7 ++++++- tests/dir_listing.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- tests/static_files.rs | 28 ++++++++++++++++++++++++++++ tests/toml/config.toml | 10 ++++++++-- 14 files changed, 364 insertions(+), 84 deletions(-) diff --git a/.github/workflows/devel.yml b/.github/workflows/devel.yml index d4b6f8d..4825534 100644 --- a/.github/workflows/devel.yml +++ b/.github/workflows/devel.yml @@ -136,7 +136,7 @@ jobs: echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - name: Cache cargo registry and git trees - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 - name: Show command used for Cargo run: | diff --git a/Cargo.lock b/Cargo.lock index 3f99e10..230c01a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,6 +874,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -919,6 +925,17 @@ dependencies = [ ] [[package]] +name = "serde_json" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] name = "serde_repr" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1037,6 +1054,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_ignored", + "serde_json", "serde_repr", "signal-hook", "signal-hook-tokio", diff --git a/Cargo.toml b/Cargo.toml index 0fe0235..3ff5f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ windows-sys = { version = "0.36.1", features = [ "Win32_Foundation", "Win32_Netw [dev-dependencies] bytes = "1.1" +serde_json = "1.0" [profile.release] codegen-units = 1 diff --git a/src/directory_listing.rs b/src/directory_listing.rs index 188a35b..c9ab5bb 100644 --- a/src/directory_listing.rs +++ b/src/directory_listing.rs @@ -11,11 +11,22 @@ use std::future::Future; use std::io; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; +use structopt::clap::arg_enum; use crate::Result; +arg_enum! { + #[derive(Debug, Serialize, Deserialize, Clone)] + #[serde(rename_all = "lowercase")] + /// Directory listing output format for file entries. + pub enum DirListFmt { + Html, + Json, + } +} + /// Provides directory listing support for the current request. -/// Note that this function highly depends on `static_files::get_composed_metadata()` function +/// Note that this function highly depends on `static_files::composed_file_metadata()` function /// which must be called first. See `static_files::handle()` for more details. pub fn auto_index<'a>( method: &'a Method, @@ -23,20 +34,28 @@ pub fn auto_index<'a>( uri_query: Option<&'a str>, filepath: &'a Path, dir_listing_order: u8, + dir_listing_format: &'a DirListFmt, ) -> impl Future, StatusCode>> + Send + 'a { let is_head = method == Method::HEAD; // Note: it's safe to call `parent()` here since `filepath` // value always refer to a path with file ending and under // a root directory boundary. - // See `get_composed_metadata()` function which sanitizes the requested + // See `composed_file_metadata()` function which sanitizes the requested // path before to be delegated here. let parent = filepath.parent().unwrap_or(filepath); tokio::fs::read_dir(parent).then(move |res| match res { - Ok(entries) => Either::Left(async move { - match read_dir_entries(entries, current_path, uri_query, is_head, dir_listing_order) - .await + Ok(dir_reader) => Either::Left(async move { + match read_dir_entries( + dir_reader, + current_path, + uri_query, + is_head, + dir_listing_order, + dir_listing_format, + ) + .await { Ok(resp) => Ok(resp), Err(err) => { @@ -76,23 +95,43 @@ pub fn auto_index<'a>( const STYLE: &str = r#""#; const FOOTER: &str = r#""#; +const DATETIME_FORMAT_UTC: &str = "%FT%TZ"; +const DATETIME_FORMAT_LOCAL: &str = "%F %T"; + +/// Defines a file entry and its properties. +struct FileEntry { + name: String, + name_encoded: String, + modified: Option>, + filesize: u64, + uri: Option, +} + +/// Defines sorting attributes for file entries. +struct SortingAttr<'a> { + name: &'a str, + last_modified: &'a str, + size: &'a str, +} + /// It reads a list of directory entries and create an index page content. /// Otherwise it returns a status error. async fn read_dir_entries( - mut file_entries: tokio::fs::ReadDir, + mut dir_reader: tokio::fs::ReadDir, base_path: &str, uri_query: Option<&str>, is_head: bool, - mut dir_listing_order: u8, + mut order_code: u8, + content_format: &DirListFmt, ) -> Result> { let mut dirs_count: usize = 0; let mut files_count: usize = 0; - let mut files_found: Vec<(String, String, u64, Option)> = vec![]; + let mut file_entries: Vec = vec![]; - while let Some(entry) = file_entries.next_entry().await? { - let meta = entry.metadata().await?; + while let Some(dir_entry) = dir_reader.next_entry().await? { + let meta = dir_entry.metadata().await?; - let name = entry + let name = dir_entry .file_name() .into_string() .map_err(|err| anyhow::anyhow!(err.into_string().unwrap_or_default()))?; @@ -107,7 +146,7 @@ async fn read_dir_entries( filesize = meta.len(); files_count += 1; } else if meta.file_type().is_symlink() { - let m = tokio::fs::symlink_metadata(entry.path().canonicalize()?).await?; + let m = tokio::fs::symlink_metadata(dir_entry.path().canonicalize()?).await?; if m.is_dir() { name_encoded += "/"; dirs_count += 1; @@ -134,6 +173,7 @@ async fn read_dir_entries( 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(); @@ -142,18 +182,25 @@ async fn read_dir_entries( } base_str.push('/'); } + base_str.push_str(&name_encoded); uri = Some(base_str); } let modified = match parse_last_modified(meta.modified()?) { - Ok(local_dt) => local_dt.format("%F %T").to_string(), + Ok(local_dt) => Some(local_dt), Err(err) => { tracing::error!("error determining file last modified: {:?}", err); - String::from("-") + None } }; - files_found.push((name_encoded, modified, filesize, uri)); + file_entries.push(FileEntry { + name, + name_encoded, + modified, + filesize, + uri, + }); } // Check the query request uri for a sorting type. E.g https://blah/?sort=5 @@ -164,7 +211,7 @@ async fn read_dir_entries( if let Some(sort) = parts.next() { if sort.0 == "sort" && !sort.1.trim().is_empty() { match sort.1.parse::() { - Ok(order_code) => dir_listing_order = order_code, + Ok(code) => order_code = code, Err(err) => { tracing::debug!( "sorting: query value error when converting to u8: {:?}", @@ -177,63 +224,145 @@ async fn read_dir_entries( } } - let html = create_auto_index( - base_path, - dirs_count, - files_count, - dir_listing_order, - &mut files_found, - )?; - let mut resp = Response::new(Body::empty()); + + // Handle directory listing content format + let content = match content_format { + DirListFmt::Json => { + // JSON + resp.headers_mut() + .typed_insert(ContentType::from(mime::APPLICATION_JSON)); + + json_auto_index(&mut file_entries, order_code)? + } + // HTML (default) + _ => { + resp.headers_mut() + .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8)); + + html_auto_index( + base_path, + dirs_count, + files_count, + &mut file_entries, + order_code, + )? + } + }; + resp.headers_mut() - .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8)); - resp.headers_mut() - .typed_insert(ContentLength(html.len() as u64)); + .typed_insert(ContentLength(content.len() as u64)); // We skip the body for HEAD requests if is_head { return Ok(resp); } - *resp.body_mut() = Body::from(html); + *resp.body_mut() = Body::from(content); Ok(resp) } -/// Create an auto index html content. -fn create_auto_index( - base_path: &str, +/// Create an auto index in JSON format. +fn json_auto_index(entries: &mut [FileEntry], order_code: u8) -> Result { + sort_file_entries(entries, order_code); + + let mut json = String::from('['); + + for entry in entries { + let file_size = &entry.filesize; + let file_name = &entry.name; + let is_empty = *file_size == 0_u64; + let file_type = if is_empty { "directory" } else { "file" }; + let file_modified = &entry.modified; + + json.push('{'); + json.push_str(format!("\"name\":{},", json_quote_str(file_name.as_str())).as_str()); + json.push_str(format!("\"type\":\"{}\",", file_type).as_str()); + + let file_modified_str = file_modified.map_or("".to_owned(), |local_dt| { + local_dt + .with_timezone(&Utc) + .format(DATETIME_FORMAT_UTC) + .to_string() + }); + json.push_str(format!("\"mtime\":\"{}\"", file_modified_str).as_str()); + + if !is_empty { + json.push_str(format!(",\"size\":{}", file_size).as_str()); + } + json.push_str("},"); + } + + json.pop(); + json.push(']'); + + Ok(json) +} + +/// Quotes a string value. +fn json_quote_str(s: &str) -> String { + let mut r = String::from("\""); + for c in s.chars() { + match c { + '\\' => r.push_str("\\\\"), + '\u{0008}' => r.push_str("\\b"), + '\u{000c}' => r.push_str("\\f"), + '\n' => r.push_str("\\n"), + '\r' => r.push_str("\\r"), + '\t' => r.push_str("\\t"), + '"' => r.push_str("\\\""), + c if c.is_control() => r.push_str(format!("\\u{:04x}", c as u32).as_str()), + c => r.push(c), + }; + } + r.push('\"'); + r +} + +/// Create an auto index in HTML format. +fn html_auto_index<'a>( + base_path: &'a str, dirs_count: usize, files_count: usize, - dir_listing_order: u8, - files_found: &mut Vec<(String, String, u64, Option)>, + entries: &'a mut [FileEntry], + order_code: u8, ) -> Result { - // Sorting the files by an specific order code and create the table header - let table_header = create_table_header(sort_files(files_found, dir_listing_order)); + let sort_attrs = sort_file_entries(entries, order_code); - // Prepare table row + // Create the table header specifying every order code column + let table_header = format!( + r#"NameLast modifiedSize"#, + sort_attrs.name, sort_attrs.last_modified, sort_attrs.size, + ); + + // Prepare table row template let mut table_row = String::new(); if base_path != "/" { table_row = String::from(r#"../"#); } - for file in files_found { - let (file_name, file_modified, file_size, uri) = file; - let mut filesize_str = file_size + for entry in entries { + let file_name = &entry.name_encoded; + let file_modified = &entry.modified; + let file_uri = &entry.uri.clone().unwrap_or_else(|| file_name.to_owned()); + let file_name_decoded = percent_decode_str(file_name).decode_utf8()?.to_string(); + let mut filesize = entry + .filesize .file_size(file_size_opts::DECIMAL) .map_err(anyhow::Error::msg)?; - if *file_size == 0_u64 { - filesize_str = String::from("-"); + if entry.filesize == 0_u64 { + filesize = String::from("-"); } - let file_uri = uri.clone().unwrap_or_else(|| file_name.to_owned()); - let file_name_decoded = percent_decode_str(file_name).decode_utf8()?.to_string(); + let file_modified_str = file_modified.map_or("-".to_owned(), |local_dt| { + local_dt.format(DATETIME_FORMAT_LOCAL).to_string() + }); table_row = format!( "{}{}{}{}", - table_row, file_uri, file_name_decoded, file_modified, filesize_str + table_row, file_uri, file_name_decoded, file_modified_str, filesize ); } @@ -256,66 +385,56 @@ fn create_auto_index( Ok(html_page) } -/// Create a table header providing the sorting attributes. -fn create_table_header(sorting_attrs: (String, String, String)) -> String { - let (name, last_modified, size) = sorting_attrs; - format!( - r#"NameLast modifiedSize"#, - name, last_modified, size, - ) -} - -/// Sort a list of files by an specific order code. -fn sort_files( - files: &mut [(String, String, u64, Option)], - order_code: u8, -) -> (String, String, String) { +/// Sort a list of file entries by a specific order code. +fn sort_file_entries(files: &mut [FileEntry], order_code: u8) -> SortingAttr<'_> { // Default sorting type values - let mut name = "0".to_owned(); - let mut last_modified = "2".to_owned(); - let mut size = "4".to_owned(); + let mut name = "0"; + let mut last_modified = "2"; + let mut size = "4"; files.sort_by(|a, b| match order_code { // Name (asc, desc) 0 => { - name = "1".to_owned(); - a.0.to_lowercase().cmp(&b.0.to_lowercase()) + name = "1"; + a.name.to_lowercase().cmp(&b.name.to_lowercase()) } 1 => { - name = "0".to_owned(); - b.0.to_lowercase().cmp(&a.0.to_lowercase()) + name = "0"; + b.name.to_lowercase().cmp(&a.name.to_lowercase()) } // Modified (asc, desc) 2 => { - last_modified = "3".to_owned(); - a.1.cmp(&b.1) + last_modified = "3"; + a.modified.cmp(&b.modified) } 3 => { - last_modified = "2".to_owned(); - b.1.cmp(&a.1) + last_modified = "2"; + b.modified.cmp(&a.modified) } // File size (asc, desc) 4 => { - size = "5".to_owned(); - a.2.cmp(&b.2) + size = "5"; + a.filesize.cmp(&b.filesize) } 5 => { - size = "4".to_owned(); - b.2.cmp(&a.2) + size = "4"; + b.filesize.cmp(&a.filesize) } // Unordered _ => Ordering::Equal, }); - (name, last_modified, size) + SortingAttr { + name, + last_modified, + size, + } } -fn parse_last_modified( - modified: SystemTime, -) -> Result, Box> { +fn parse_last_modified(modified: SystemTime) -> Result> { let since_epoch = modified.duration_since(UNIX_EPOCH)?; // HTTP times don't have nanosecond precision, so we truncate // the modification time. diff --git a/src/handler.rs b/src/handler.rs index ec1ee08..6f5f9bd 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,8 +3,9 @@ use hyper::{header::WWW_AUTHENTICATE, Body, Method, Request, Response, StatusCod use std::{future::Future, net::IpAddr, net::SocketAddr, path::PathBuf, sync::Arc}; use crate::{ - basic_auth, compression, control_headers, cors, custom_headers, error_page, fallback_page, - redirects, rewrites, security_headers, + basic_auth, compression, control_headers, cors, custom_headers, + directory_listing::DirListFmt, + error_page, fallback_page, redirects, rewrites, security_headers, settings::Advanced, static_files::{self, HandleOpts}, Error, Result, @@ -18,6 +19,7 @@ pub struct RequestHandlerOpts { pub compression_static: bool, pub dir_listing: bool, pub dir_listing_order: u8, + pub dir_listing_format: DirListFmt, pub cors: Option, pub security_headers: bool, pub cache_control_headers: bool, @@ -53,6 +55,7 @@ impl RequestHandler { let uri_query = uri.query(); let dir_listing = self.opts.dir_listing; let dir_listing_order = self.opts.dir_listing_order; + let dir_listing_format = &self.opts.dir_listing_format; let log_remote_addr = self.opts.log_remote_address; let redirect_trailing_slash = self.opts.redirect_trailing_slash; let compression_static = self.opts.compression_static; @@ -190,6 +193,7 @@ impl RequestHandler { uri_query, dir_listing, dir_listing_order, + dir_listing_format, redirect_trailing_slash, compression_static, }) diff --git a/src/server.rs b/src/server.rs index cd3a608..2d931be 100644 --- a/src/server.rs +++ b/src/server.rs @@ -150,6 +150,10 @@ impl Server { let dir_listing_order = general.directory_listing_order; tracing::info!("directory listing order code: {}", dir_listing_order); + // Directory listing format + let dir_listing_format = general.directory_listing_format; + tracing::info!("directory listing format: {}", dir_listing_format); + // Cache control headers option let cache_control_headers = general.cache_control_headers; tracing::info!("cache control headers: enabled={}", cache_control_headers); @@ -191,6 +195,7 @@ impl Server { compression_static, dir_listing, dir_listing_order, + dir_listing_format, cors, security_headers, cache_control_headers, diff --git a/src/settings/cli.rs b/src/settings/cli.rs index 2614ec5..5c9ae26 100644 --- a/src/settings/cli.rs +++ b/src/settings/cli.rs @@ -3,6 +3,8 @@ use std::path::PathBuf; use structopt::StructOpt; +use crate::directory_listing::DirListFmt; + /// General server configuration available in CLI and config file options. #[derive(Debug, StructOpt)] #[structopt(about, author)] @@ -155,6 +157,17 @@ pub struct General { #[structopt( long, + required_if("directory_listing", "true"), + possible_values = &DirListFmt::variants(), + default_value = "html", + env = "SERVER_DIRECTORY_LISTING_FORMAT", + case_insensitive = true + )] + /// Specify a content format for directory listing entries. Formats supported: "html" or "json". Default "html". + pub directory_listing_format: DirListFmt, + + #[structopt( + long, parse(try_from_str), required_if("http2", "true"), default_value_if("http2", Some("true"), "true"), diff --git a/src/settings/file.rs b/src/settings/file.rs index d65f370..e2ecc60 100644 --- a/src/settings/file.rs +++ b/src/settings/file.rs @@ -6,6 +6,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use std::path::Path; use std::{collections::BTreeSet, path::PathBuf}; +use crate::directory_listing::DirListFmt; use crate::{helpers, Context, Result}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -116,6 +117,7 @@ pub struct General { // Directory listing pub directory_listing: Option, pub directory_listing_order: Option, + pub directory_listing_format: Option, // Basich Authentication pub basic_auth: Option, diff --git a/src/settings/mod.rs b/src/settings/mod.rs index c61dcd6..39ae749 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -79,6 +79,7 @@ impl Settings { let mut cors_expose_headers = opts.cors_expose_headers; let mut directory_listing = opts.directory_listing; let mut directory_listing_order = opts.directory_listing_order; + let mut directory_listing_format = opts.directory_listing_format; let mut basic_auth = opts.basic_auth; let mut fd = opts.fd; let mut threads_multiplier = opts.threads_multiplier; @@ -165,6 +166,9 @@ impl Settings { if let Some(v) = general.directory_listing_order { directory_listing_order = v } + if let Some(v) = general.directory_listing_format { + directory_listing_format = v + } if let Some(ref v) = general.basic_auth { basic_auth = v.to_owned() } @@ -308,6 +312,7 @@ impl Settings { cors_expose_headers, directory_listing, directory_listing_order, + directory_listing_format, basic_auth, fd, threads_multiplier, diff --git a/src/static_files.rs b/src/static_files.rs index b6c65dd..80f126d 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -23,6 +23,7 @@ use tokio::fs::File as TkFile; use tokio::io::AsyncSeekExt; use tokio_util::io::poll_read_buf; +use crate::directory_listing::DirListFmt; use crate::{compression_static, directory_listing, Result}; /// Defines all options needed by the static-files handler. @@ -34,6 +35,7 @@ pub struct HandleOpts<'a> { pub uri_query: Option<&'a str>, pub dir_listing: bool, pub dir_listing_order: u8, + pub dir_listing_format: &'a DirListFmt, pub redirect_trailing_slash: bool, pub compression_static: bool, } @@ -108,6 +110,7 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response, bool), opts.uri_query, file_path.as_ref(), opts.dir_listing_order, + opts.dir_listing_format, ) .await?; diff --git a/tests/compression_static.rs b/tests/compression_static.rs index cebc38c..c697c6f 100644 --- a/tests/compression_static.rs +++ b/tests/compression_static.rs @@ -10,7 +10,10 @@ mod tests { use http::Method; use std::path::PathBuf; - use static_web_server::static_files::{self, HandleOpts}; + use static_web_server::{ + directory_listing::DirListFmt, + static_files::{self, HandleOpts}, + }; fn public_dir() -> PathBuf { PathBuf::from("docker/public/") @@ -37,6 +40,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: true, }) @@ -89,6 +93,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: true, }) diff --git a/tests/dir_listing.rs b/tests/dir_listing.rs index d70b345..a27fd33 100644 --- a/tests/dir_listing.rs +++ b/tests/dir_listing.rs @@ -7,9 +7,13 @@ mod tests { use headers::HeaderMap; use http::{Method, StatusCode}; + use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; - use static_web_server::static_files::{self, HandleOpts}; + use static_web_server::{ + directory_listing::DirListFmt, + static_files::{self, HandleOpts}, + }; const METHODS: [Method; 8] = [ Method::CONNECT, @@ -40,6 +44,7 @@ mod tests { uri_query: None, dir_listing: true, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -68,6 +73,7 @@ mod tests { uri_query: None, dir_listing: true, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -106,6 +112,7 @@ mod tests { uri_query: None, dir_listing: true, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, }) @@ -144,6 +151,7 @@ mod tests { uri_query: None, dir_listing: true, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, }) @@ -172,6 +180,7 @@ mod tests { uri_query: None, dir_listing: true, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -200,4 +209,66 @@ mod tests { } } } + + #[tokio::test] + async fn dir_listing_json_format() { + #[derive(Serialize, Deserialize)] + struct FileEntry { + name: String, + #[serde(rename = "type")] + typed: String, + mtime: String, + size: Option, + } + + for method in METHODS { + match static_files::handle(&HandleOpts { + method: &method, + headers: &HeaderMap::new(), + base_path: &root_dir("tests/fixtures/public/"), + uri_path: "/", + uri_query: None, + dir_listing: true, + dir_listing_order: 1, + dir_listing_format: &DirListFmt::Json, + redirect_trailing_slash: true, + compression_static: false, + }) + .await + { + Ok((mut res, _)) => { + assert_eq!(res.status(), 200); + assert_eq!(res.headers()["content-type"], "application/json"); + + 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(); + + if method == Method::GET { + let entries: Vec = serde_json::from_str(body_str).unwrap(); + assert_eq!(entries.len(), 2); + + let first_entry = entries.first().unwrap(); + assert_eq!(first_entry.name, "spécial directöry"); + assert_eq!(first_entry.typed, "directory"); + assert_eq!(first_entry.mtime.is_empty(), false); + assert!(first_entry.size.is_none()); + + let last_entry = entries.last().unwrap(); + assert_eq!(last_entry.name, "index.html.gz"); + assert_eq!(last_entry.typed, "file"); + assert_eq!(last_entry.mtime.is_empty(), false); + assert!(last_entry.size.unwrap() > 300); + } else { + assert!(body_str.is_empty()); + } + } + Err(status) => { + assert!(method != Method::GET && method != Method::HEAD); + assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED); + } + } + } + } } diff --git a/tests/static_files.rs b/tests/static_files.rs index 9908119..725f9ee 100644 --- a/tests/static_files.rs +++ b/tests/static_files.rs @@ -13,6 +13,7 @@ mod tests { use static_web_server::{ compression, + directory_listing::DirListFmt, static_files::{self, HandleOpts}, }; @@ -30,6 +31,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -70,6 +72,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -111,6 +114,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -136,6 +140,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 0, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -162,6 +167,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 0, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -187,6 +193,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 0, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: false, compression_static: false, }) @@ -217,6 +224,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -262,6 +270,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -289,6 +298,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -319,6 +329,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -349,6 +360,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -382,6 +394,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -413,6 +426,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -442,6 +456,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -470,6 +485,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -512,6 +528,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -571,6 +588,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -633,6 +651,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -675,6 +694,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -718,6 +738,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -753,6 +774,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -798,6 +820,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -840,6 +863,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -885,6 +909,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -928,6 +953,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -966,6 +992,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) @@ -1015,6 +1042,7 @@ mod tests { uri_query: None, dir_listing: false, dir_listing_order: 6, + dir_listing_format: &DirListFmt::Html, redirect_trailing_slash: true, compression_static: false, }) diff --git a/tests/toml/config.toml b/tests/toml/config.toml index d096246..eafaa3e 100644 --- a/tests/toml/config.toml +++ b/tests/toml/config.toml @@ -3,7 +3,7 @@ #### Address & Root dir host = "::" port = 8787 -root = "docker/public" +root = "tests/fixtures/public" #### Logging log-level = "trace" @@ -28,7 +28,13 @@ security-headers = true cors-allow-origins = "" #### Directory listing -directory-listing = false +directory-listing = true + +#### Directory listing sorting code +directory-listing-order = 1 + +#### Directory listing content format +directory-listing-format = "json" #### Basich Authentication basic-auth = "" -- libgit2 1.7.2