Skip to content

Commit

Permalink
Split serve_dir.rs into multiple files (#250)
Browse files Browse the repository at this point in the history
* move tests to their own module

* refactor opening the file

* break things into smaller files

* add missing feature
  • Loading branch information
davidpdrsn authored Apr 25, 2022
1 parent 4904b86 commit 0c68c06
Show file tree
Hide file tree
Showing 9 changed files with 1,722 additions and 1,648 deletions.
2 changes: 1 addition & 1 deletion tower-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ auth = ["base64"]
catch-panic = ["tracing", "futures-util/std"]
cors = []
follow-redirect = ["iri-string", "tower/util"]
fs = ["tokio/fs", "tokio-util/io", "tokio/io-util", "mime_guess", "mime", "percent-encoding", "httpdate", "set-status"]
fs = ["tokio/fs", "tokio-util/io", "tokio/io-util", "mime_guess", "mime", "percent-encoding", "httpdate", "set-status", "futures-util/alloc"]
map-request-body = []
map-response-body = []
metrics = ["tokio/time"]
Expand Down
183 changes: 2 additions & 181 deletions tower-http/src/services/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,139 +2,31 @@

use bytes::Bytes;
use futures_util::Stream;
use http::{HeaderMap, StatusCode};
use http::HeaderMap;
use http_body::Body;
use httpdate::HttpDate;
use pin_project_lite::pin_project;
use std::fs::Metadata;
use std::{ffi::OsStr, path::PathBuf};
use std::{
io,
pin::Pin,
task::{Context, Poll},
time::SystemTime,
};
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, Take};
use tokio_util::io::ReaderStream;

mod serve_dir;
mod serve_file;

// default capacity 64KiB
const DEFAULT_CAPACITY: usize = 65536;

use crate::content_encoding::{Encoding, QValue, SupportedEncodings};

pub use self::{
serve_dir::{
future::ResponseFuture as ServeFileSystemResponseFuture,
DefaultServeDirFallback,
// The response body and future are used for both ServeDir and ServeFile
ResponseBody as ServeFileSystemResponseBody,
ResponseFuture as ServeFileSystemResponseFuture,
ServeDir,
},
serve_file::ServeFile,
};

#[derive(Clone, Copy, Debug, Default)]
struct PrecompressedVariants {
gzip: bool,
deflate: bool,
br: bool,
}

impl SupportedEncodings for PrecompressedVariants {
fn gzip(&self) -> bool {
self.gzip
}

fn deflate(&self) -> bool {
self.deflate
}

fn br(&self) -> bool {
self.br
}
}

// Returns the preferred_encoding encoding and modifies the path extension
// to the corresponding file extension for the encoding.
fn preferred_encoding(
path: &mut PathBuf,
negotiated_encoding: &[(Encoding, QValue)],
) -> Option<Encoding> {
let preferred_encoding = Encoding::preferred_encoding(negotiated_encoding);
if let Some(file_extension) =
preferred_encoding.and_then(|encoding| encoding.to_file_extension())
{
let new_extension = path
.extension()
.map(|extension| {
let mut os_string = extension.to_os_string();
os_string.push(file_extension);
os_string
})
.unwrap_or_else(|| file_extension.to_os_string());
path.set_extension(new_extension);
}
preferred_encoding
}

// Attempts to open the file with any of the possible negotiated_encodings in the
// preferred order. If none of the negotiated_encodings have a corresponding precompressed
// file the uncompressed file is used as a fallback.
async fn open_file_with_fallback(
mut path: PathBuf,
mut negotiated_encoding: Vec<(Encoding, QValue)>,
) -> io::Result<(File, Option<Encoding>)> {
let (file, encoding) = loop {
// Get the preferred encoding among the negotiated ones.
let encoding = preferred_encoding(&mut path, &negotiated_encoding);
match (File::open(&path).await, encoding) {
(Ok(file), maybe_encoding) => break (file, maybe_encoding),
(Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
// Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
// to reset the path before the next iteration.
path.set_extension(OsStr::new(""));
// Remove the encoding from the negotiated_encodings since the file doesn't exist
negotiated_encoding
.retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
continue;
}
(Err(err), _) => return Err(err),
};
};
Ok((file, encoding))
}

// Attempts to get the file metadata with any of the possible negotiated_encodings in the
// preferred order. If none of the negotiated_encodings have a corresponding precompressed
// file the uncompressed file is used as a fallback.
async fn file_metadata_with_fallback(
mut path: PathBuf,
mut negotiated_encoding: Vec<(Encoding, QValue)>,
) -> io::Result<(Metadata, Option<Encoding>)> {
let (file, encoding) = loop {
// Get the preferred encoding among the negotiated ones.
let encoding = preferred_encoding(&mut path, &negotiated_encoding);
match (tokio::fs::metadata(&path).await, encoding) {
(Ok(file), maybe_encoding) => break (file, maybe_encoding),
(Err(err), Some(encoding)) if err.kind() == io::ErrorKind::NotFound => {
// Remove the extension corresponding to a precompressed file (.gz, .br, .zz)
// to reset the path before the next iteration.
path.set_extension(OsStr::new(""));
// Remove the encoding from the negotiated_encodings since the file doesn't exist
negotiated_encoding
.retain(|(negotiated_encoding, _)| *negotiated_encoding != encoding);
continue;
}
(Err(err), _) => return Err(err),
};
};
Ok((file, encoding))
}

pin_project! {
// NOTE: This could potentially be upstreamed to `http-body`.
/// Adapter that turns an `impl AsyncRead` to an `impl Body`.
Expand Down Expand Up @@ -189,74 +81,3 @@ where
Poll::Ready(Ok(None))
}
}

struct LastModified(HttpDate);

impl From<SystemTime> for LastModified {
fn from(time: SystemTime) -> Self {
LastModified(time.into())
}
}

struct IfUnmodifiedSince(HttpDate);
struct IfModifiedSince(HttpDate);

impl IfModifiedSince {
/// Check if the supplied time means the resource has been modified.
fn is_modified(&self, last_modified: &LastModified) -> bool {
self.0 < last_modified.0
}

/// convert a header value into a IfModifiedSince, invalid values are silentely ignored
fn from_header_value(value: &http::header::HeaderValue) -> Option<IfModifiedSince> {
std::str::from_utf8(value.as_bytes())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfModifiedSince(time.into()))
}
}

impl IfUnmodifiedSince {
/// Check if the supplied time passes the precondtion.
fn precondition_passes(&self, last_modified: &LastModified) -> bool {
self.0 >= last_modified.0
}

/// convert a header value into a IfModifiedSince, invalid values are silentely ignored
fn from_header_value(value: &http::header::HeaderValue) -> Option<IfUnmodifiedSince> {
std::str::from_utf8(value.as_bytes())
.ok()
.and_then(|value| httpdate::parse_http_date(&value).ok())
.map(|time| IfUnmodifiedSince(time.into()))
}
}

fn check_modified_headers(
modified: Option<&LastModified>,
if_unmodified_since: Option<IfUnmodifiedSince>,
if_modified_since: Option<IfModifiedSince>,
) -> Option<StatusCode> {
if let Some(since) = if_unmodified_since {
let precondition = modified
.as_ref()
.map(|time| since.precondition_passes(time))
.unwrap_or(false);

if !precondition {
return Some(StatusCode::PRECONDITION_FAILED);
}
}

if let Some(since) = if_modified_since {
let unmodified = modified
.as_ref()
.map(|time| !since.is_modified(&time))
// no last_modified means its always modified
.unwrap_or(false);
if unmodified {
return Some(StatusCode::NOT_MODIFIED);
}
}

None
}
Loading

0 comments on commit 0c68c06

Please sign in to comment.