diff --git a/examples/file.rs b/examples/file.rs index a0cf2afa4..2b5963628 100644 --- a/examples/file.rs +++ b/examples/file.rs @@ -1,5 +1,6 @@ #![deny(warnings)] +use warp::header::Conditionals; use warp::Filter; #[tokio::main] @@ -10,12 +11,22 @@ async fn main() { .and(warp::path::end()) .and(warp::fs::file("./README.md")); + // try GET /dyn/Cargo.toml or GET /dyn/README.md + let dynamic_file = warp::get() + .and(warp::path::path("dyn")) + .and(warp::path::param::()) + .and(warp::header::conditionals()) + .and_then(|file_name: String, conditionals: Conditionals| { + warp::reply::file(file_name, conditionals) + }); + // dir already requires GET... let examples = warp::path("ex").and(warp::fs::dir("./examples/")); // GET / => README.md + // Get /dyn/{file} => ./{file} // GET /ex/... => ./examples/.. - let routes = readme.or(examples); + let routes = readme.or(dynamic_file).or(examples); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } diff --git a/src/filters/fs.rs b/src/filters/fs.rs index 0949b66ec..a8e751cf5 100644 --- a/src/filters/fs.rs +++ b/src/filters/fs.rs @@ -1,7 +1,6 @@ //! File System Filters use std::cmp; -use std::convert::Infallible; use std::fs::Metadata; use std::future::Future; use std::io; @@ -14,8 +13,7 @@ use bytes::{Bytes, BytesMut}; use futures_util::future::Either; use futures_util::{future, ready, stream, FutureExt, Stream, StreamExt, TryFutureExt}; use headers::{ - AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, IfModifiedSince, IfRange, - IfUnmodifiedSince, LastModified, Range, + AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt, LastModified, Range, }; use http::StatusCode; use hyper::Body; @@ -26,6 +24,7 @@ use tokio::io::AsyncSeekExt; use tokio_util::io::poll_read_buf; use crate::filter::{Filter, FilterClone, One}; +use crate::header::{conditionals, Conditionals}; use crate::reject::{self, Rejection}; use crate::reply::{Reply, Response}; @@ -135,84 +134,6 @@ fn sanitize_path(base: impl AsRef, tail: &str) -> Result, - if_unmodified_since: Option, - if_range: Option, - range: Option, -} - -enum Cond { - NoBody(Response), - WithBody(Option), -} - -impl Conditionals { - fn check(self, last_modified: Option) -> Cond { - if let Some(since) = self.if_unmodified_since { - let precondition = last_modified - .map(|time| since.precondition_passes(time.into())) - .unwrap_or(false); - - tracing::trace!( - "if-unmodified-since? {:?} vs {:?} = {}", - since, - last_modified, - precondition - ); - if !precondition { - let mut res = Response::new(Body::empty()); - *res.status_mut() = StatusCode::PRECONDITION_FAILED; - return Cond::NoBody(res); - } - } - - if let Some(since) = self.if_modified_since { - tracing::trace!( - "if-modified-since? header = {:?}, file = {:?}", - since, - last_modified - ); - let unmodified = last_modified - .map(|time| !since.is_modified(time.into())) - // no last_modified means its always modified - .unwrap_or(false); - if unmodified { - let mut res = Response::new(Body::empty()); - *res.status_mut() = StatusCode::NOT_MODIFIED; - return Cond::NoBody(res); - } - } - - if let Some(if_range) = self.if_range { - tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified); - let can_range = !if_range.is_modified(None, last_modified.as_ref()); - - if !can_range { - return Cond::WithBody(None); - } - } - - Cond::WithBody(self.range) - } -} - -fn conditionals() -> impl Filter, Error = Infallible> + Copy { - crate::header::optional2() - .and(crate::header::optional2()) - .and(crate::header::optional2()) - .and(crate::header::optional2()) - .map( - |if_modified_since, if_unmodified_since, if_range, range| Conditionals { - if_modified_since, - if_unmodified_since, - if_range, - range, - }, - ) -} - /// A file response. #[derive(Debug)] pub struct File { @@ -247,7 +168,7 @@ impl File { // Silly wrapper since Arc doesn't implement AsRef ;_; #[derive(Clone, Debug)] -struct ArcPath(Arc); +pub(crate) struct ArcPath(pub(crate) Arc); impl AsRef for ArcPath { fn as_ref(&self) -> &Path { @@ -261,7 +182,7 @@ impl Reply for File { } } -fn file_reply( +pub(crate) fn file_reply( path: ArcPath, conditionals: Conditionals, ) -> impl Future> + Send { @@ -310,9 +231,10 @@ fn file_conditional( let mut len = meta.len(); let modified = meta.modified().ok().map(LastModified::from); + use crate::header::ConditionalBody; let resp = match conditionals.check(modified) { - Cond::NoBody(resp) => resp, - Cond::WithBody(range) => { + ConditionalBody::NoBody(resp) => resp, + ConditionalBody::WithBody(range) => { bytes_range(range, len) .map(|(start, end)| { let sub_len = end - start; @@ -343,7 +265,7 @@ fn file_conditional( resp }) - .unwrap_or_else(|BadRange| { + .unwrap_or_else(|_: BadRange| { // bad byte range let mut resp = Response::new(Body::empty()); *resp.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; @@ -502,9 +424,10 @@ unit_error! { #[cfg(test)] mod tests { - use super::sanitize_path; use bytes::BytesMut; + use super::sanitize_path; + #[test] fn test_sanitize_path() { let base = "/var/www"; diff --git a/src/filters/header.rs b/src/filters/header.rs index 0c535a38b..51bcba4dd 100644 --- a/src/filters/header.rs +++ b/src/filters/header.rs @@ -8,12 +8,16 @@ use std::convert::Infallible; use std::str::FromStr; use futures_util::future; -use headers::{Header, HeaderMapExt}; +use headers::{ + Header, HeaderMapExt, IfModifiedSince, IfRange, IfUnmodifiedSince, LastModified, Range, +}; use http::header::HeaderValue; -use http::HeaderMap; +use http::{HeaderMap, StatusCode}; +use hyper::Body; use crate::filter::{filter_fn, filter_fn_one, Filter, One}; use crate::reject::{self, Rejection}; +use crate::reply::Response; /// Create a `Filter` that tries to parse the specified header. /// @@ -228,3 +232,94 @@ pub fn value( pub fn headers_cloned() -> impl Filter, Error = Infallible> + Copy { filter_fn_one(|route| future::ok(route.headers().clone())) } + +/// Create a `Filter` that returns a container for conditional headers +/// +/// # Example +/// ``` +/// use warp::Filter; +/// use warp::fs::Conditionals; +/// +/// let headers = warp::header::conditionals() +/// .and_then(|conditionals: Conditionals| { +/// warp::reply::file("index.html", conditionals) +/// }); +/// ``` +pub fn conditionals() -> impl Filter, Error = Infallible> + Copy { + optional2() + .and(optional2()) + .and(optional2()) + .and(optional2()) + .map( + |if_modified_since, if_unmodified_since, if_range, range| Conditionals { + if_modified_since, + if_unmodified_since, + if_range, + range, + }, + ) +} + +/// Utilized conditional headers to determine response-content +#[derive(Debug, Default)] +pub struct Conditionals { + if_modified_since: Option, + if_unmodified_since: Option, + if_range: Option, + range: Option, +} + +pub(crate) enum ConditionalBody { + NoBody(Response), + WithBody(Option), +} + +impl Conditionals { + pub(crate) fn check(self, last_modified: Option) -> ConditionalBody { + if let Some(since) = self.if_unmodified_since { + let precondition = last_modified + .map(|time| since.precondition_passes(time.into())) + .unwrap_or(false); + + tracing::trace!( + "if-unmodified-since? {:?} vs {:?} = {}", + since, + last_modified, + precondition + ); + if !precondition { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return ConditionalBody::NoBody(res); + } + } + + if let Some(since) = self.if_modified_since { + tracing::trace!( + "if-modified-since? header = {:?}, file = {:?}", + since, + last_modified + ); + let unmodified = last_modified + .map(|time| !since.is_modified(time.into())) + // no last_modified means its always modified + .unwrap_or(false); + if unmodified { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::NOT_MODIFIED; + return ConditionalBody::NoBody(res); + } + } + + if let Some(if_range) = self.if_range { + tracing::trace!("if-range? {:?} vs {:?}", if_range, last_modified); + let can_range = !if_range.is_modified(None, last_modified.as_ref()); + + if !can_range { + return ConditionalBody::WithBody(None); + } + } + + ConditionalBody::WithBody(self.range) + } +} diff --git a/src/reply.rs b/src/reply.rs index 74dee278d..edd3e64d7 100644 --- a/src/reply.rs +++ b/src/reply.rs @@ -37,6 +37,9 @@ use std::borrow::Cow; use std::convert::TryFrom; use std::error::Error as StdError; use std::fmt; +use std::future::Future; +use std::path::PathBuf; +use std::sync::Arc; use crate::generic::{Either, One}; use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; @@ -392,6 +395,30 @@ impl Reply for WithHeader { } } +/// Serve a file as an `impl Reply` +/// +/// # Example +/// +/// ``` +/// use warp::Filter; +/// use warp::fs::Conditionals; +/// use std::path::PathBuf; +/// +/// let route = warp::any() +/// .and(warp::path::param::()) +/// .and(warp::header::conditionals()) +/// .and_then(|file: String, conditionals: Conditionals| { +/// warp::reply::file(PathBuf::from(file), conditionals) +/// }); +/// ``` +pub fn file( + path: impl Into, + conditionals: crate::header::Conditionals, +) -> impl Future> + Send { + let path = Arc::new(path.into()); + crate::fs::file_reply(crate::fs::ArcPath(path), conditionals) +} + impl Reply for ::http::Response where Body: From,