Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement warp::reply::file for dynamic file service #1049

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 12 additions & 1 deletion examples/file.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![deny(warnings)]

use warp::header::Conditionals;
use warp::Filter;

#[tokio::main]
Expand All @@ -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::<String>())
.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;
}
97 changes: 10 additions & 87 deletions src/filters/fs.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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};

Expand Down Expand Up @@ -135,84 +134,6 @@ fn sanitize_path(base: impl AsRef<Path>, tail: &str) -> Result<PathBuf, Rejectio
Ok(buf)
}

#[derive(Debug)]
struct Conditionals {
if_modified_since: Option<IfModifiedSince>,
if_unmodified_since: Option<IfUnmodifiedSince>,
if_range: Option<IfRange>,
range: Option<Range>,
}

enum Cond {
NoBody(Response),
WithBody(Option<Range>),
}

impl Conditionals {
fn check(self, last_modified: Option<LastModified>) -> 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<Extract = One<Conditionals>, 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 {
Expand Down Expand Up @@ -247,7 +168,7 @@ impl File {

// Silly wrapper since Arc<PathBuf> doesn't implement AsRef<Path> ;_;
#[derive(Clone, Debug)]
struct ArcPath(Arc<PathBuf>);
pub(crate) struct ArcPath(pub(crate) Arc<PathBuf>);

impl AsRef<Path> for ArcPath {
fn as_ref(&self) -> &Path {
Expand All @@ -261,7 +182,7 @@ impl Reply for File {
}
}

fn file_reply(
pub(crate) fn file_reply(
path: ArcPath,
conditionals: Conditionals,
) -> impl Future<Output = Result<File, Rejection>> + Send {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
99 changes: 97 additions & 2 deletions src/filters/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -228,3 +232,94 @@ pub fn value(
pub fn headers_cloned() -> impl Filter<Extract = One<HeaderMap>, 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<Extract = One<Conditionals>, 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<IfModifiedSince>,
if_unmodified_since: Option<IfUnmodifiedSince>,
if_range: Option<IfRange>,
range: Option<Range>,
}

pub(crate) enum ConditionalBody {
NoBody(Response),
WithBody(Option<Range>),
}

impl Conditionals {
pub(crate) fn check(self, last_modified: Option<LastModified>) -> 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)
}
}
27 changes: 27 additions & 0 deletions src/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -392,6 +395,30 @@ impl<T: Reply> Reply for WithHeader<T> {
}
}

/// 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::<String>())
/// .and(warp::header::conditionals())
/// .and_then(|file: String, conditionals: Conditionals| {
/// warp::reply::file(PathBuf::from(file), conditionals)
/// });
/// ```
pub fn file(
path: impl Into<PathBuf>,
conditionals: crate::header::Conditionals,
) -> impl Future<Output = Result<crate::fs::File, crate::Rejection>> + Send {
let path = Arc::new(path.into());
crate::fs::file_reply(crate::fs::ArcPath(path), conditionals)
}

impl<T: Send> Reply for ::http::Response<T>
where
Body: From<T>,
Expand Down