Skip to content

Commit

Permalink
refactor: improve performance when serving static files (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
joseluisq committed Apr 9, 2024
1 parent 90b6032 commit a451a93
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 24 deletions.
5 changes: 5 additions & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ harness = false
name = "static_files"
path = "static_files.rs"
harness = false

[[bench]]
name = "http_ext"
path = "http_ext.rs"
harness = false
16 changes: 16 additions & 0 deletions benches/http_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};

use hyper::Method;
use static_web_server::http_ext::MethodExt;

fn is_allowed_benchmark(c: &mut Criterion) {
let method = Method::default();
c.bench_with_input(
BenchmarkId::new("method_input", &method),
&method,
|b, _| b.iter(|| method.is_allowed()),
);
}

criterion_group!(http_ext_bench, is_allowed_benchmark);
criterion_main!(http_ext_bench);
2 changes: 2 additions & 0 deletions src/compression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pub fn create_encoding_header(existing: Option<HeaderValue>, coding: ContentCodi
}

/// Try to get the prefered `content-encoding` via the `accept-encoding` header.
#[inline(always)]
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();
Expand Down Expand Up @@ -236,6 +237,7 @@ where
}

impl From<Body> for CompressableBody<Body, hyper::Error> {
#[inline(always)]
fn from(body: Body) -> Self {
CompressableBody { body }
}
Expand Down
38 changes: 22 additions & 16 deletions src/control_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,7 @@ const CACHE_EXT_ONE_YEAR: [&str; 32] = [

/// It appends a `Cache-Control` header to a response if that one is part of a set of file types.
pub fn append_headers(uri: &str, resp: &mut Response<Body>) {
// Default max-age value in seconds (one day)
let mut max_age = MAX_AGE_ONE_DAY;

if let Some(extension) = uri_file_extension(uri) {
if CACHE_EXT_ONE_HOUR.binary_search(&extension).is_ok() {
max_age = MAX_AGE_ONE_HOUR;
} else if CACHE_EXT_ONE_YEAR.binary_search(&extension).is_ok() {
max_age = MAX_AGE_ONE_YEAR;
}
}

let max_age = get_max_age(uri);
resp.headers_mut().insert(
"cache-control",
format!(
Expand All @@ -50,16 +40,32 @@ pub fn append_headers(uri: &str, resp: &mut Response<Body>) {
/// Gets the file extension for a URI.
///
/// This assumes the extension contains a single dot. e.g. for "/file.tar.gz" it returns "gz".
fn uri_file_extension(uri: &str) -> Option<&str> {
#[inline(always)]
fn get_file_extension(uri: &str) -> Option<&str> {
uri.rsplit_once('.').map(|(_, rest)| rest)
}

#[inline(always)]
fn get_max_age(uri: &str) -> u64 {
// Default max-age value in seconds (one day)
let mut max_age = MAX_AGE_ONE_DAY;

if let Some(extension) = get_file_extension(uri) {
if CACHE_EXT_ONE_HOUR.binary_search(&extension).is_ok() {
max_age = MAX_AGE_ONE_HOUR;
} else if CACHE_EXT_ONE_YEAR.binary_search(&extension).is_ok() {
max_age = MAX_AGE_ONE_YEAR;
}
}
max_age
}

#[cfg(test)]
mod tests {
use hyper::{Body, Response, StatusCode};

use super::{
append_headers, uri_file_extension, CACHE_EXT_ONE_HOUR, CACHE_EXT_ONE_YEAR,
append_headers, get_file_extension, CACHE_EXT_ONE_HOUR, CACHE_EXT_ONE_YEAR,
MAX_AGE_ONE_DAY, MAX_AGE_ONE_HOUR, MAX_AGE_ONE_YEAR,
};

Expand Down Expand Up @@ -114,8 +120,8 @@ mod tests {

#[test]
fn find_uri_extension() {
assert_eq!(uri_file_extension("/potato.zip"), Some("zip"));
assert_eq!(uri_file_extension("/potato."), Some(""));
assert_eq!(uri_file_extension("/"), None);
assert_eq!(get_file_extension("/potato.zip"), Some("zip"));
assert_eq!(get_file_extension("/potato."), Some(""));
assert_eq!(get_file_extension("/"), None);
}
}
20 changes: 13 additions & 7 deletions src/file_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

use hyper::StatusCode;
use percent_encoding::percent_decode_str;
use std::path::{Component, Path, PathBuf};
use std::{
borrow::Cow,
path::{Component, Path, PathBuf},
};

/// SWS Path extensions trait.
pub(crate) trait PathExt {
Expand All @@ -27,16 +30,19 @@ impl PathExt for Path {
}
}

/// Sanitizes a base/tail paths and then it returns an unified one.
pub(crate) fn sanitize_path(base: &Path, tail: &str) -> Result<PathBuf, StatusCode> {
let path_decoded = match percent_decode_str(tail.trim_start_matches('/')).decode_utf8() {
Ok(p) => p,
fn decode_tail_path(tail: &str) -> Result<Cow<'_, str>, StatusCode> {
match percent_decode_str(tail.trim_start_matches('/')).decode_utf8() {
Ok(p) => Ok(p),
Err(err) => {
tracing::debug!("dir: failed to decode route={:?}: {:?}", tail, err);
return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE);
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE)
}
};
}
}

/// Sanitizes a base/tail paths and then it returns an unified one.
pub(crate) fn sanitize_path(base: &Path, tail: &str) -> Result<PathBuf, StatusCode> {
let path_decoded = decode_tail_path(tail)?;
let path_decoded = Path::new(&*path_decoded);
let mut full_path = base.to_path_buf();
tracing::trace!("dir: base={:?}, route={:?}", full_path, path_decoded);
Expand Down
11 changes: 10 additions & 1 deletion src/http_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,30 @@ pub trait MethodExt {

impl MethodExt for Method {
/// Checks if the HTTP method is allowed (supported) by SWS.
#[inline(always)]
fn is_allowed(&self) -> bool {
HTTP_SUPPORTED_METHODS.iter().any(|h| self == h)
for method in HTTP_SUPPORTED_METHODS {
if method == self {
return true;
}
}
false
}

/// Checks if the HTTP method is `GET`.
#[inline(always)]
fn is_get(&self) -> bool {
self == Method::GET
}

/// Checks if the HTTP method is `HEAD`.
#[inline(always)]
fn is_head(&self) -> bool {
self == Method::HEAD
}

/// Checks if the HTTP method is `OPTIONS`.
#[inline(always)]
fn is_options(&self) -> bool {
self == Method::OPTIONS
}
Expand Down
1 change: 1 addition & 0 deletions src/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use {

#[cfg(unix)]
#[cfg_attr(docsrs, doc(cfg(unix)))]
#[inline]
/// It creates a common list of signals stream for `SIGTERM`, `SIGINT` and `SIGQUIT` to be observed.
pub fn create_signals() -> Result<Signals> {
Ok(Signals::new([SIGHUP, SIGTERM, SIGINT, SIGQUIT])?)
Expand Down

0 comments on commit a451a93

Please sign in to comment.