feat: directory listing format support (#151)
option: `--directory-listing-format`
formats supported: `html`, `json`
default: `html`
resolves #128
Diff
.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(-)
@@ -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: |
@@ -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",
@@ -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
@@ -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")]
pub enum DirListFmt {
Html,
Json,
}
}
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<Output = Result<Response<Body>, StatusCode>> + Send + 'a {
let is_head = method == Method::HEAD;
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#"<style>html{background-color:#fff;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:20rem;text-rendering:optimizeLegibility;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%}body{padding:1rem;font-family:Consolas,'Liberation Mono',Menlo,monospace;font-size:.875rem;max-width:70rem;margin:0 auto;color:#4a4a4a;font-weight:400;line-height:1.5}h1{margin:0;padding:0;font-size:1.375rem;line-height:1.25;margin-bottom:0.5rem;}table{width:100%;border-spacing: 0;}table th,table td{padding:.2rem .5rem;white-space:nowrap;vertical-align:top}table th a,table td a{display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:95%;vertical-align:top}table tr:hover td{background-color:#f5f5f5}footer{padding-top:0.5rem}table tr th{text-align:left;}</style>"#;
const FOOTER: &str = r#"<footer>Powered by <a target="_blank" href="https://github.com/joseluisq/static-web-server">static-web-server</a> | MIT & Apache 2.0</footer>"#;
const DATETIME_FORMAT_UTC: &str = "%FT%TZ";
const DATETIME_FORMAT_LOCAL: &str = "%F %T";
struct FileEntry {
name: String,
name_encoded: String,
modified: Option<DateTime<Local>>,
filesize: u64,
uri: Option<String>,
}
struct SortingAttr<'a> {
name: &'a str,
last_modified: &'a str,
size: &'a str,
}
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<Response<Body>> {
let mut dirs_count: usize = 0;
let mut files_count: usize = 0;
let mut files_found: Vec<(String, String, u64, Option<String>)> = vec![];
let mut file_entries: Vec<FileEntry> = 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,
});
}
@@ -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::<u8>() {
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());
let content = match content_format {
DirListFmt::Json => {
resp.headers_mut()
.typed_insert(ContentType::from(mime::APPLICATION_JSON));
json_auto_index(&mut file_entries, order_code)?
}
_ => {
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));
if is_head {
return Ok(resp);
}
*resp.body_mut() = Body::from(html);
*resp.body_mut() = Body::from(content);
Ok(resp)
}
fn create_auto_index(
base_path: &str,
fn json_auto_index(entries: &mut [FileEntry], order_code: u8) -> Result<String> {
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)
}
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
}
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<String>)>,
entries: &'a mut [FileEntry],
order_code: u8,
) -> Result<String> {
let table_header = create_table_header(sort_files(files_found, dir_listing_order));
let sort_attrs = sort_file_entries(entries, order_code);
let table_header = format!(
r#"<thead><tr><th><a href="?sort={}">Name</a></th><th style="width:160px;"><a href="?sort={}">Last modified</a></th><th style="width:120px;text-align:right;"><a href="?sort={}">Size</a></th></tr></thead>"#,
sort_attrs.name, sort_attrs.last_modified, sort_attrs.size,
);
let mut table_row = String::new();
if base_path != "/" {
table_row = String::from(r#"<tr><td colspan="3"><a href="../">../</a></td></tr>"#);
}
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!(
"{}<tr><td><a href=\"{}\">{}</a></td><td>{}</td><td align=\"right\">{}</td></tr>",
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)
}
fn create_table_header(sorting_attrs: (String, String, String)) -> String {
let (name, last_modified, size) = sorting_attrs;
format!(
r#"<thead><tr><th><a href="?sort={}">Name</a></th><th style="width:160px;"><a href="?sort={}">Last modified</a></th><th style="width:120px;text-align:right;"><a href="?sort={}">Size</a></th></tr></thead>"#,
name, last_modified, size,
)
}
fn sort_files(
files: &mut [(String, String, u64, Option<String>)],
order_code: u8,
) -> (String, String, String) {
fn sort_file_entries(files: &mut [FileEntry], order_code: u8) -> SortingAttr<'_> {
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 {
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())
}
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)
}
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)
}
_ => Ordering::Equal,
});
(name, last_modified, size)
SortingAttr {
name,
last_modified,
size,
}
}
fn parse_last_modified(
modified: SystemTime,
) -> Result<DateTime<Local>, Box<dyn std::error::Error>> {
fn parse_last_modified(modified: SystemTime) -> Result<DateTime<Local>> {
let since_epoch = modified.duration_since(UNIX_EPOCH)?;
@@ -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<cors::Configured>,
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,
})
@@ -150,6 +150,10 @@ impl Server {
let dir_listing_order = general.directory_listing_order;
tracing::info!("directory listing order code: {}", dir_listing_order);
let dir_listing_format = general.directory_listing_format;
tracing::info!("directory listing format: {}", dir_listing_format);
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,
@@ -3,6 +3,8 @@
use std::path::PathBuf;
use structopt::StructOpt;
use crate::directory_listing::DirListFmt;
#[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
)]
pub directory_listing_format: DirListFmt,
#[structopt(
long,
parse(try_from_str),
required_if("http2", "true"),
default_value_if("http2", Some("true"), "true"),
@@ -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 {
pub directory_listing: Option<bool>,
pub directory_listing_order: Option<u8>,
pub directory_listing_format: Option<DirListFmt>,
pub basic_auth: Option<String>,
@@ -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,
@@ -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};
@@ -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<Body>, bool),
opts.uri_query,
file_path.as_ref(),
opts.dir_listing_order,
opts.dir_listing_format,
)
.await?;
@@ -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,
})
@@ -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<usize>,
}
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<FileEntry> = 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);
}
}
}
}
}
@@ -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,
})
@@ -3,7 +3,7 @@
host = "::"
port = 8787
root = "docker/public"
root = "tests/fixtures/public"
log-level = "trace"
@@ -28,7 +28,13 @@ security-headers = true
cors-allow-origins = ""
directory-listing = false
directory-listing = true
directory-listing-order = 1
directory-listing-format = "json"
basic-auth = ""