Support for serving pre-compressed files (#139)
* feat: support for serving pre-compressed files via new boolean `compression-static` option
* refactor: continue workflow when the pre-compressed file is not found
* chore: preliminary precompressed response
* refactor: pre-compressed file variant handling
* refactor: bump up pinned rust version to 1.59.0 on ci
* refactor: compression_static module
* refactor: optimize file metadata search for pre-compressed variant
* tests: compression static
* refactor: some tests and static file variables
Diff
.github/workflows/devel.yml | 2 +-
.gitignore | 1 +-
src/compression.rs | 54 ++++++-----
src/compression_static.rs | 57 +++++++++++-
src/directory_listing.rs | 4 +-
src/handler.rs | 8 +-
src/lib.rs | 1 +-
src/server.rs | 5 +-
src/settings/cli.rs | 10 ++-
src/settings/file.rs | 3 +-
src/settings/mod.rs | 5 +-
src/static_files.rs | 186 +++++++++++++++++++++++++------------
tests/compression_static.rs | 122 ++++++++++++++++++++++++-
tests/dir_listing.rs | 12 +-
tests/fixtures/public/index.html.gz | Bin 0 -> 332 bytes
tests/static_files.rs | 77 ++++++++++-----
tests/toml/config.toml | 3 +-
17 files changed, 439 insertions(+), 111 deletions(-)
@@ -48,7 +48,7 @@ jobs:
- build: pinned
os: ubuntu-20.04
rust: 1.56.1
rust: 1.59.0
- build: stable
os: ubuntu-20.04
rust: stable
@@ -26,3 +26,4 @@ docs/*/**.html
!sample.env
!/docs
!/tests/fixtures/**/*
@@ -44,6 +44,14 @@ pub const TEXT_MIME_TYPES: [&str; 24] = [
"application/wasm",
];
pub fn get_prefered_encoding(headers: &HeaderMap<HeaderValue>) -> Option<ContentCoding> {
if let Some(ref accept_encoding) = headers.typed_get::<AcceptEncoding>() {
return accept_encoding.prefered_encoding();
}
None
}
@@ -59,28 +67,26 @@ pub fn auto(
}
if let Some(ref accept_encoding) = headers.typed_get::<AcceptEncoding>() {
if let Some(encoding) = accept_encoding.prefered_encoding() {
if let Some(content_type) = resp.headers().typed_get::<ContentType>() {
let content_type = &content_type.to_string();
if !TEXT_MIME_TYPES.iter().any(|h| *h == content_type) {
return Ok(resp);
}
if let Some(encoding) = get_prefered_encoding(headers) {
if let Some(content_type) = resp.headers().typed_get::<ContentType>() {
let content_type = &content_type.to_string();
if !TEXT_MIME_TYPES.iter().any(|h| *h == content_type) {
return Ok(resp);
}
}
if encoding == ContentCoding::GZIP {
let (head, body) = resp.into_parts();
return Ok(gzip(head, body.into()));
}
if encoding == ContentCoding::DEFLATE {
let (head, body) = resp.into_parts();
return Ok(deflate(head, body.into()));
}
if encoding == ContentCoding::BROTLI {
let (head, body) = resp.into_parts();
return Ok(brotli(head, body.into()));
}
if encoding == ContentCoding::GZIP {
let (head, body) = resp.into_parts();
return Ok(gzip(head, body.into()));
}
if encoding == ContentCoding::DEFLATE {
let (head, body) = resp.into_parts();
return Ok(deflate(head, body.into()));
}
if encoding == ContentCoding::BROTLI {
let (head, body) = resp.into_parts();
return Ok(brotli(head, body.into()));
}
}
@@ -93,6 +99,8 @@ pub fn gzip(
mut head: http::response::Parts,
body: CompressableBody<Body, hyper::Error>,
) -> Response<Body> {
tracing::trace!("compressing response body on the fly using gzip");
let body = Body::wrap_stream(ReaderStream::new(GzipEncoder::new(StreamReader::new(body))));
let header = create_encoding_header(head.headers.remove(CONTENT_ENCODING), ContentCoding::GZIP);
head.headers.remove(CONTENT_LENGTH);
@@ -106,6 +114,8 @@ pub fn deflate(
mut head: http::response::Parts,
body: CompressableBody<Body, hyper::Error>,
) -> Response<Body> {
tracing::trace!("compressing response body on the fly using deflate");
let body = Body::wrap_stream(ReaderStream::new(DeflateEncoder::new(StreamReader::new(
body,
))));
@@ -124,6 +134,8 @@ pub fn brotli(
mut head: http::response::Parts,
body: CompressableBody<Body, hyper::Error>,
) -> Response<Body> {
tracing::trace!("compressing response body on the fly using brotli");
let body = Body::wrap_stream(ReaderStream::new(BrotliEncoder::new(StreamReader::new(
body,
))));
@@ -135,7 +147,7 @@ pub fn brotli(
}
fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCoding) -> HeaderValue {
pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCoding) -> HeaderValue {
if let Some(val) = existing {
if let Ok(str_val) = val.to_str() {
return HeaderValue::from_str(&[str_val, ", ", coding.to_static()].concat())
@@ -0,0 +1,57 @@
use headers::{ContentCoding, HeaderMap, HeaderValue};
use std::{fs::Metadata, path::PathBuf, sync::Arc};
use crate::{
compression,
static_files::{file_metadata, ArcPath},
};
pub async fn precompressed_variant(
file_path: PathBuf,
headers: &HeaderMap<HeaderValue>,
) -> Option<(ArcPath, Metadata, &str)> {
let mut precompressed = None;
tracing::trace!(
"preparing pre-compressed file path variant of {}",
file_path.display()
);
let precomp_ext = match compression::get_prefered_encoding(headers) {
Some(ContentCoding::GZIP | ContentCoding::DEFLATE) => Some("gz"),
Some(ContentCoding::BROTLI) => Some("br"),
_ => None,
};
if precomp_ext.is_none() {
tracing::trace!("preferred encoding based on the file extension was not determined");
}
if let Some(ext) = precomp_ext {
let mut filepath_precomp = file_path;
let filename = filepath_precomp.file_name().unwrap().to_str().unwrap();
let precomp_file_name = [filename, ".", ext].concat();
filepath_precomp.set_file_name(precomp_file_name);
tracing::trace!(
"getting metadata for pre-compressed file variant {}",
filepath_precomp.display()
);
if let Ok((meta, _)) = file_metadata(&filepath_precomp).await {
tracing::trace!("pre-compressed file variant found, serving it directly");
let encoding = if ext == "gz" { "gzip" } else { ext };
precompressed = Some((ArcPath(Arc::new(filepath_precomp)), meta, encoding));
}
}
precompressed
}
@@ -14,7 +14,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use crate::Result;
pub fn auto_index<'a>(
method: &'a Method,
@@ -28,7 +28,7 @@ pub fn auto_index<'a>(
let parent = filepath.parent().unwrap_or(filepath);
@@ -15,6 +15,7 @@ pub struct RequestHandlerOpts {
pub root_dir: PathBuf,
pub compression: bool,
pub compression_static: bool,
pub dir_listing: bool,
pub dir_listing_order: u8,
pub cors: Option<cors::Configured>,
@@ -54,6 +55,7 @@ impl RequestHandler {
let dir_listing_order = self.opts.dir_listing_order;
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;
let mut cors_headers: Option<http::HeaderMap> = None;
@@ -144,6 +146,7 @@ impl RequestHandler {
}
}
if let Some(advanced) = &self.opts.advanced_opts {
if let Some(parts) = redirects::get_redirection(uri_path, &advanced.redirects) {
@@ -188,10 +191,11 @@ impl RequestHandler {
dir_listing,
dir_listing_order,
redirect_trailing_slash,
compression_static,
})
.await
{
Ok(mut resp) => {
Ok((mut resp, is_precompressed)) => {
if let Some(cors_headers) = cors_headers {
if !cors_headers.is_empty() {
@@ -203,7 +207,7 @@ impl RequestHandler {
}
if self.opts.compression {
if self.opts.compression && !is_precompressed {
resp = match compression::auto(method, headers, resp) {
Ok(res) => res,
Err(err) => {
@@ -11,6 +11,7 @@ extern crate serde;
pub mod basic_auth;
pub mod compression;
pub mod compression_static;
pub mod control_headers;
pub mod cors;
pub mod custom_headers;
@@ -138,6 +138,10 @@ impl Server {
let compression = general.compression;
tracing::info!("auto compression: enabled={}", compression);
let compression_static = general.compression_static;
tracing::info!("compression static: enabled={}", compression_static);
let dir_listing = general.directory_listing;
tracing::info!("directory listing: enabled={}", dir_listing);
@@ -183,6 +187,7 @@ impl Server {
opts: Arc::from(RequestHandlerOpts {
root_dir,
compression,
compression_static,
dir_listing,
dir_listing_order,
cors,
@@ -118,6 +118,16 @@ pub struct General {
#[structopt(
long,
parse(try_from_str),
default_value = "false",
env = "SERVER_COMPRESSION_STATIC"
)]
pub compression_static: bool,
#[structopt(
long,
short = "z",
parse(try_from_str),
default_value = "false",
@@ -93,6 +93,9 @@ pub struct General {
pub compression: Option<bool>,
pub compression_static: Option<bool>,
pub page404: Option<PathBuf>,
pub page50x: Option<PathBuf>,
@@ -67,6 +67,7 @@ impl Settings {
let mut config_file = opts.config_file.clone();
let mut cache_control_headers = opts.cache_control_headers;
let mut compression = opts.compression;
let mut compression_static = opts.compression_static;
let mut page404 = opts.page404;
let mut page50x = opts.page50x;
let mut http2 = opts.http2;
@@ -127,6 +128,9 @@ impl Settings {
if let Some(v) = general.compression {
compression = v
}
if let Some(v) = general.compression_static {
compression_static = v
}
if let Some(v) = general.page404 {
page404 = v
}
@@ -288,6 +292,7 @@ impl Settings {
config_file,
cache_control_headers,
compression,
compression_static,
page404,
page50x,
http2,
@@ -3,12 +3,13 @@
use bytes::{Bytes, BytesMut};
use futures_util::future::Either;
use futures_util::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt};
use futures_util::{future, ready, stream, FutureExt, Stream, StreamExt};
use headers::{
AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMap, HeaderMapExt, HeaderValue,
IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range,
};
use hyper::{Body, Method, Response, StatusCode};
use http::header::CONTENT_LENGTH;
use hyper::{header::CONTENT_ENCODING, Body, Method, Response, StatusCode};
use percent_encoding::percent_decode_str;
use std::fs::Metadata;
use std::future::Future;
@@ -23,7 +24,7 @@ use tokio::fs::File as TkFile;
use tokio::io::AsyncSeekExt;
use tokio_util::io::poll_read_buf;
use crate::{directory_listing, Result};
use crate::{compression_static, directory_listing, Result};
#[derive(Clone, Debug)]
@@ -45,11 +46,12 @@ pub struct HandleOpts<'a> {
pub dir_listing: bool,
pub dir_listing_order: u8,
pub redirect_trailing_slash: bool,
pub compression_static: bool,
}
pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<Response<Body>, StatusCode> {
pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<(Response<Body>, bool), StatusCode> {
let method = opts.method;
let uri_path = opts.uri_path;
@@ -58,14 +60,23 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<Response<Body>, StatusC
return Err(StatusCode::METHOD_NOT_ALLOWED);
}
let base = Arc::new(opts.base_path.into());
let (filepath, meta, auto_index) = path_from_tail(base, uri_path).await?;
let headers_opt = opts.headers;
let compression_static_opt = opts.compression_static;
let base = Arc::<PathBuf>::new(opts.base_path.into());
let file_path = sanitize_path(base.as_ref(), uri_path)?;
let (file_path, meta, is_dir, precompressed_variant) =
composed_file_metadata(file_path, headers_opt, compression_static_opt).await?;
let is_precompressed = precompressed_variant.is_some();
if opts.redirect_trailing_slash && auto_index && !uri_path.ends_with('/') {
if opts.redirect_trailing_slash && is_dir && !uri_path.ends_with('/') {
let uri = [uri_path, "/"].concat();
let loc = match HeaderValue::from_str(uri.as_str()) {
Ok(val) => val,
@@ -78,11 +89,12 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<Response<Body>, StatusC
let mut resp = Response::new(Body::empty());
resp.headers_mut().insert(hyper::header::LOCATION, loc);
*resp.status_mut() = StatusCode::PERMANENT_REDIRECT;
tracing::trace!("uri doesn't end with a slash so redirecting permanently");
return Ok(resp);
return Ok((resp, is_precompressed));
}
if method == Method::OPTIONS {
let mut resp = Response::new(Body::empty());
*resp.status_mut() = StatusCode::NO_CONTENT;
@@ -93,66 +105,138 @@ pub async fn handle<'a>(opts: &HandleOpts<'a>) -> Result<Response<Body>, StatusC
Method::GET,
]));
resp.headers_mut().typed_insert(AcceptRanges::bytes());
return Ok(resp);
return Ok((resp, is_precompressed));
}
if opts.dir_listing && auto_index && !filepath.as_ref().exists() {
return directory_listing::auto_index(
if opts.dir_listing && is_dir && !file_path.as_ref().exists() {
let resp = directory_listing::auto_index(
method,
uri_path,
opts.uri_query,
filepath.as_ref(),
file_path.as_ref(),
opts.dir_listing_order,
)
.await;
.await?;
return Ok((resp, is_precompressed));
}
file_reply(opts.headers, (filepath, &meta, auto_index)).await
if let Some(meta_precompressed) = precompressed_variant {
let (path_precomp, precomp_ext) = meta_precompressed;
let mut resp = file_reply(headers_opt, file_path, &meta, Some(path_precomp)).await?;
resp.headers_mut().remove(CONTENT_LENGTH);
resp.headers_mut()
.insert(CONTENT_ENCODING, precomp_ext.parse().unwrap());
return Ok((resp, is_precompressed));
}
let resp = file_reply(headers_opt, file_path, &meta, None).await?;
Ok((resp, is_precompressed))
}
fn path_from_tail(
base: Arc<PathBuf>,
tail: &str,
) -> impl Future<Output = Result<(ArcPath, Metadata, bool), StatusCode>> + Send {
future::ready(sanitize_path(base.as_ref(), tail)).and_then(|mut buf| async {
match tokio::fs::metadata(&buf).await {
Ok(meta) => {
let mut auto_index = false;
if meta.is_dir() {
tracing::debug!("dir: appending index.html to directory path");
buf.push("index.html");
auto_index = true;
async fn composed_file_metadata(
mut file_path: PathBuf,
headers: &HeaderMap<HeaderValue>,
compression_static: bool,
) -> Result<(ArcPath, Metadata, bool, Option<(ArcPath, &str)>), StatusCode> {
let mut tried_precompressed = false;
if compression_static {
tried_precompressed = true;
if let Some((path, meta, ext)) =
compression_static::precompressed_variant(file_path.clone(), headers).await
{
return Ok((ArcPath(Arc::new(file_path)), meta, false, Some((path, ext))));
}
}
tracing::trace!("getting metadata for file {}", file_path.display());
match file_metadata(file_path.as_ref()).await {
Ok((mut meta, is_dir)) => {
if is_dir {
tracing::debug!("dir: appending an index.html to the directory path");
file_path.push("index.html");
if let Ok(meta_res) = file_metadata(file_path.as_ref()).await {
(meta, _) = meta_res
}
tracing::trace!("dir: {:?}", buf);
Ok((ArcPath(Arc::new(buf)), meta, auto_index))
}
Err(err) => {
tracing::debug!("file not found: {} {:?}", buf.display(), err);
Err(StatusCode::NOT_FOUND)
Ok((ArcPath(Arc::new(file_path)), meta, is_dir, None))
}
Err(err) => {
if compression_static && !tried_precompressed {
if let Some((path, meta, ext)) =
compression_static::precompressed_variant(file_path.clone(), headers).await
{
return Ok((ArcPath(Arc::new(file_path)), meta, false, Some((path, ext))));
}
}
Err(err)
}
})
}
}
pub async fn file_metadata(file_path: &Path) -> Result<(Metadata, bool), StatusCode> {
match tokio::fs::metadata(file_path).await {
Ok(meta) => {
let is_dir = meta.is_dir();
tracing::trace!("file found: {:?}", file_path);
Ok((meta, is_dir))
}
Err(err) => {
tracing::debug!("file not found: {:?} {:?}", file_path, err);
Err(StatusCode::NOT_FOUND)
}
}
}
fn file_reply<'a>(
headers: &'a HeaderMap<HeaderValue>,
res: (ArcPath, &'a Metadata, bool),
path: ArcPath,
meta: &'a Metadata,
path_precompressed: Option<ArcPath>,
) -> impl Future<Output = Result<Response<Body>, StatusCode>> + Send + 'a {
let (path, meta, auto_index) = res;
let conditionals = get_conditional_headers(headers);
TkFile::open(path.clone()).then(move |res| match res {
Ok(file) => Either::Left(file_conditional(file, path, meta, auto_index, conditionals)),
let file_path = path_precompressed.unwrap_or_else(|| path.clone());
TkFile::open(file_path).then(move |res| match res {
Ok(file) => Either::Left(file_conditional(file, path, meta, conditionals)),
Err(err) => {
let status = match err.kind() {
io::ErrorKind::NotFound => {
tracing::debug!("file not found: {:?}", path.as_ref().display());
tracing::debug!(
"file can't be opened or not found: {:?}",
path.as_ref().display()
);
StatusCode::NOT_FOUND
}
io::ErrorKind::PermissionDenied => {
@@ -187,6 +271,7 @@ fn get_conditional_headers(header_list: &HeaderMap<HeaderValue>) -> Conditionals
}
}
fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, StatusCode> {
let path_decoded = match percent_decode_str(tail.trim_start_matches('/')).decode_utf8() {
Ok(p) => p,
@@ -198,7 +283,7 @@ fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, StatusCo
let path_decoded = Path::new(&*path_decoded);
let mut full_path = base.as_ref().to_path_buf();
tracing::trace!("dir? base={:?}, route={:?}", full_path, path_decoded);
tracing::trace!("dir: base={:?}, route={:?}", full_path, path_decoded);
for component in path_decoded.components() {
match component {
@@ -291,20 +376,9 @@ async fn file_conditional(
file: TkFile,
path: ArcPath,
meta: &Metadata,
auto_index: bool,
conditionals: Conditionals,
) -> Result<Response<Body>, StatusCode> {
if auto_index {
match file.metadata().await {
Ok(meta) => Ok(response_body(file, &meta, path, conditionals)),
Err(err) => {
tracing::debug!("file metadata error: {}", err);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
} else {
Ok(response_body(file, meta, path, conditionals))
}
Ok(response_body(file, meta, path, conditionals))
}
fn response_body(
@@ -0,0 +1,122 @@
#![forbid(unsafe_code)]
#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(dead_code)]
#[cfg(test)]
mod tests {
use bytes::Bytes;
use headers::HeaderMap;
use http::Method;
use std::path::PathBuf;
use static_web_server::static_files::{self, HandleOpts};
fn public_dir() -> PathBuf {
PathBuf::from("docker/public/")
}
#[tokio::test]
async fn compression_static_file_exists() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::ACCEPT_ENCODING,
"gzip, deflate, br".parse().unwrap(),
);
let index_gz_path = PathBuf::from("tests/fixtures/public/index.html.gz");
let index_gz_path_public = public_dir().join("index.html.gz");
std::fs::copy(&index_gz_path, &index_gz_path_public)
.expect("unexpected error copying fixture file");
let (mut resp, _) = static_files::handle(&HandleOpts {
method: &Method::GET,
headers: &headers,
base_path: &public_dir(),
uri_path: "index.html",
uri_query: None,
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: true,
})
.await
.expect("unexpected error response on `handle` function");
let index_gz_buf =
std::fs::read(&index_gz_path).expect("unexpected error when reading index.html.gz");
let index_gz_buf = Bytes::from(index_gz_buf);
std::fs::remove_file(index_gz_path_public).unwrap();
let headers = resp.headers();
assert_eq!(resp.status(), 200);
assert!(!headers.contains_key("content-length"));
assert_eq!(headers["content-encoding"], "gzip");
assert_eq!(headers["accept-ranges"], "bytes");
assert!(!headers["last-modified"].is_empty());
assert_eq!(
&headers["content-type"], "text/html",
"content-type is not html"
);
let body = hyper::body::to_bytes(resp.body_mut())
.await
.expect("unexpected bytes error during `body` conversion");
assert_eq!(
body, index_gz_buf,
"body and index_gz_buf are not equal in length"
);
}
#[tokio::test]
async fn compression_static_file_does_not_exist() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::ACCEPT_ENCODING,
"gzip, deflate, br".parse().unwrap(),
);
let index_path_public = public_dir().join("assets/index.html");
let (mut resp, _) = static_files::handle(&HandleOpts {
method: &Method::GET,
headers: &headers,
base_path: &public_dir().join("assets/"),
uri_path: "index.html",
uri_query: None,
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: true,
})
.await
.expect("unexpected error response on `handle` function");
let index_buf =
std::fs::read(&index_path_public).expect("unexpected error when reading index.html");
let index_buf = Bytes::from(index_buf);
let headers = resp.headers();
assert_eq!(resp.status(), 200);
assert!(headers.contains_key("content-length"));
assert_eq!(headers["accept-ranges"], "bytes");
assert!(!headers["last-modified"].is_empty());
assert_eq!(
&headers["content-type"], "text/html",
"content-type is not html"
);
let body = hyper::body::to_bytes(resp.body_mut())
.await
.expect("unexpected bytes error during `body` conversion");
assert_eq!(
body, index_buf,
"body and index_gz_buf are not equal in length"
);
}
}
@@ -41,10 +41,11 @@ mod tests {
dir_listing: true,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 308);
assert_eq!(res.headers()["location"], "/assets/");
}
@@ -68,10 +69,11 @@ mod tests {
dir_listing: true,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8");
@@ -105,10 +107,11 @@ mod tests {
dir_listing: true,
dir_listing_order: 6,
redirect_trailing_slash: false,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8");
@@ -142,10 +145,11 @@ mod tests {
dir_listing: true,
dir_listing_order: 6,
redirect_trailing_slash: false,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-type"], "text/markdown");
}
Binary files /dev/null and b/tests/fixtures/public/index.html.gz differ
@@ -22,7 +22,7 @@ mod tests {
#[tokio::test]
async fn handle_file() {
let mut res = static_files::handle(&HandleOpts {
let (mut res, _) = static_files::handle(&HandleOpts {
method: &Method::GET,
headers: &HeaderMap::new(),
base_path: &root_dir(),
@@ -31,6 +31,7 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
.expect("unexpected error response on `handle` function");
@@ -61,7 +62,7 @@ mod tests {
#[tokio::test]
async fn handle_file_head() {
let mut res = static_files::handle(&HandleOpts {
let (mut res, _) = static_files::handle(&HandleOpts {
method: &Method::HEAD,
headers: &HeaderMap::new(),
base_path: &root_dir(),
@@ -70,6 +71,7 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
.expect("unexpected error response on `handle` function");
@@ -110,6 +112,7 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
@@ -125,7 +128,7 @@ mod tests {
#[tokio::test]
async fn handle_trailing_slash_redirection() {
let mut res = static_files::handle(&HandleOpts {
let (mut res, _) = static_files::handle(&HandleOpts {
method: &Method::GET,
headers: &HeaderMap::new(),
base_path: &root_dir(),
@@ -134,6 +137,7 @@ mod tests {
dir_listing: false,
dir_listing_order: 0,
redirect_trailing_slash: true,
compression_static: false,
})
.await
.expect("unexpected error response on `handle` function");
@@ -159,10 +163,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 0,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 308);
assert_eq!(res.headers()["location"], "assets/");
}
@@ -183,10 +188,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 0,
redirect_trailing_slash: false,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
}
Err(status) => {
@@ -212,10 +218,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
if uri.is_empty() {
assert_eq!(res.status(), 308);
@@ -256,10 +263,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-length"], buf.len().to_string());
}
@@ -282,6 +290,7 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
@@ -311,10 +320,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-length"], buf.len().to_string());
res
@@ -340,10 +350,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 304);
assert_eq!(res.headers().get("content-length"), None);
let body = hyper::body::to_bytes(res.body_mut())
@@ -372,10 +383,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 200);
let body = hyper::body::to_bytes(res.body_mut())
.await
@@ -402,10 +414,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
res
}
@@ -430,10 +443,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
}
Err(_) => {
@@ -457,10 +471,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 412);
let body = hyper::body::to_bytes(res.body_mut())
@@ -498,10 +513,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => match method {
Ok((mut res, _)) => match method {
Method::GET | Method::HEAD => {
let buf = fs::read(root_dir().join("index.html"))
@@ -556,10 +572,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
let res = compression::auto(method, &headers, res)
.expect("unexpected bytes error during body compression");
@@ -617,10 +634,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
@@ -658,10 +676,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 416);
assert_eq!(
res.headers()["content-range"],
@@ -700,10 +719,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(res) => {
Ok((res, _)) => {
assert_eq!(res.status(), 200);
assert_eq!(res.headers()["content-length"], buf.len().to_string());
assert_eq!(res.headers().get("content-range"), None);
@@ -734,10 +754,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
@@ -778,10 +799,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
@@ -819,10 +841,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 416);
assert_eq!(
res.headers()["content-range"],
@@ -863,10 +886,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 416);
assert_eq!(
res.headers()["content-range"],
@@ -905,10 +929,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 200);
let body = hyper::body::to_bytes(res.body_mut())
.await
@@ -942,10 +967,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
@@ -990,10 +1016,11 @@ mod tests {
dir_listing: false,
dir_listing_order: 6,
redirect_trailing_slash: true,
compression_static: false,
})
.await
{
Ok(mut res) => {
Ok((mut res, _)) => {
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
@@ -52,6 +52,9 @@ log-remote-address = false
redirect-trailing-slash = true
compression-static = true