From 50f2df35451a905d3ec84f77ff9c43799eb067fd Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Wed, 7 Feb 2024 22:03:16 -0500 Subject: [PATCH] Rework fs filters to be more configurable --- examples/compression.rs | 8 +- examples/dir.rs | 2 +- examples/file.rs | 4 +- examples/returning.rs | 4 +- examples/unix_socket.rs | 2 +- src/filter/boxed.rs | 2 +- src/filters/compression.rs | 6 +- src/filters/fs.rs | 356 ++++++++++++++++++++++++++++++++----- tests/fs.rs | 168 +++++++++++++++-- 9 files changed, 487 insertions(+), 65 deletions(-) diff --git a/examples/compression.rs b/examples/compression.rs index 1a52c7a7d..47849dbe6 100644 --- a/examples/compression.rs +++ b/examples/compression.rs @@ -4,25 +4,25 @@ use warp::Filter; #[tokio::main] async fn main() { - let file = warp::path("todos").and(warp::fs::file("./examples/todos.rs")); + let file = warp::path("todos").and(warp::fs::config().file("./examples/todos.rs")); // NOTE: You could double compress something by adding a compression // filter here, a la // ``` // let file = warp::path("todos") - // .and(warp::fs::file("./examples/todos.rs")) + // .and(warp::fs::config().file("./examples/todos.rs")) // .with(warp::compression::brotli()); // ``` // This would result in a browser error, or downloading a file whose contents // are compressed - let dir = warp::path("ws_chat").and(warp::fs::file("./examples/websockets_chat.rs")); + let dir = warp::path("ws_chat").and(warp::fs::config().file("./examples/websockets_chat.rs")); let file_and_dir = warp::get() .and(file.or(dir)) .with(warp::compression::gzip()); let examples = warp::path("ex") - .and(warp::fs::dir("./examples/")) + .and(warp::fs::config().dir("./examples/")) .with(warp::compression::deflate()); // GET /todos => gzip -> toods.rs diff --git a/examples/dir.rs b/examples/dir.rs index 30261a220..13d890cce 100644 --- a/examples/dir.rs +++ b/examples/dir.rs @@ -4,7 +4,7 @@ async fn main() { pretty_env_logger::init(); - warp::serve(warp::fs::dir("examples/dir")) + warp::serve(warp::fs::config().dir("examples/dir")) .run(([127, 0, 0, 1], 3030)) .await; } diff --git a/examples/file.rs b/examples/file.rs index a0cf2afa4..ea53613f4 100644 --- a/examples/file.rs +++ b/examples/file.rs @@ -8,10 +8,10 @@ async fn main() { let readme = warp::get() .and(warp::path::end()) - .and(warp::fs::file("./README.md")); + .and(warp::fs::config().file("./README.md")); // dir already requires GET... - let examples = warp::path("ex").and(warp::fs::dir("./examples/")); + let examples = warp::path("ex").and(warp::fs::config().dir("./examples/")); // GET / => README.md // GET /ex/... => ./examples/.. diff --git a/examples/returning.rs b/examples/returning.rs index f4f61e60f..c860d7b3f 100644 --- a/examples/returning.rs +++ b/examples/returning.rs @@ -5,7 +5,9 @@ use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; // Boxing the filters will use dynamic dispatch and speed up compilation while // making it slightly slower at runtime. pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { - warp::path("assets").and(warp::fs::dir("./assets")).boxed() + warp::path("assets") + .and(warp::fs::config().dir("./assets")) + .boxed() } // Option 2: impl Filter + Clone diff --git a/examples/unix_socket.rs b/examples/unix_socket.rs index 521aeead2..8bae9ab3d 100644 --- a/examples/unix_socket.rs +++ b/examples/unix_socket.rs @@ -10,7 +10,7 @@ async fn main() { let listener = UnixListener::bind("/tmp/warp.sock").unwrap(); let incoming = UnixListenerStream::new(listener); - warp::serve(warp::fs::dir("examples/dir")) + warp::serve(warp::fs::config().dir("examples/dir")) .run_incoming(incoming) .await; } diff --git a/src/filter/boxed.rs b/src/filter/boxed.rs index 5dd43cbca..c87add629 100644 --- a/src/filter/boxed.rs +++ b/src/filter/boxed.rs @@ -22,7 +22,7 @@ use crate::reject::Rejection; /// /// pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { /// warp::path("assets") -/// .and(warp::fs::dir("./assets")) +/// .and(warp::fs::config().dir("./assets")) /// .boxed() /// } /// ``` diff --git a/src/filters/compression.rs b/src/filters/compression.rs index 244e76835..c0b0cefa8 100644 --- a/src/filters/compression.rs +++ b/src/filters/compression.rs @@ -63,7 +63,7 @@ pub struct Compression { /// /// let route = warp::get() /// .and(warp::path::end()) -/// .and(warp::fs::file("./README.md")) +/// .and(warp::fs::config().file("./README.md")) /// .with(warp::compression::gzip()); /// ``` #[cfg(feature = "compression-gzip")] @@ -92,7 +92,7 @@ pub fn gzip() -> Compression Response + Copy> { /// /// let route = warp::get() /// .and(warp::path::end()) -/// .and(warp::fs::file("./README.md")) +/// .and(warp::fs::config().file("./README.md")) /// .with(warp::compression::deflate()); /// ``` #[cfg(feature = "compression-gzip")] @@ -121,7 +121,7 @@ pub fn deflate() -> Compression Response + Copy> { /// /// let route = warp::get() /// .and(warp::path::end()) -/// .and(warp::fs::file("./README.md")) +/// .and(warp::fs::config().file("./README.md")) /// .with(warp::compression::brotli()); /// ``` #[cfg(feature = "compression-brotli")] diff --git a/src/filters/fs.rs b/src/filters/fs.rs index fdfa70968..a324bc3a5 100644 --- a/src/filters/fs.rs +++ b/src/filters/fs.rs @@ -9,15 +9,17 @@ use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Arc; use std::task::Poll; +use std::time::SystemTime; 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, ETag, HeaderMapExt, IfMatch, + IfModifiedSince, IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range, }; -use http::StatusCode; +use http::header::IntoHeaderName; +use http::{HeaderMap, HeaderValue, StatusCode}; use hyper::Body; use mime_guess; use percent_encoding::percent_decode_str; @@ -29,6 +31,179 @@ use crate::filter::{Filter, FilterClone, One}; use crate::reject::{self, Rejection}; use crate::reply::{Reply, Response}; +type ConfigFn = fn(Context, &Config) -> Option; + +/// Context structure passed to ConfigFn +#[derive(Debug)] +pub struct Context { + path: ArcPath, + metadata: Metadata, +} + +impl Context { + fn new(path: &ArcPath, metadata: &Metadata) -> Self { + Self { + path: path.clone(), + metadata: metadata.clone(), + } + } + + /// Return the path reference to the file on disk + pub fn path(&self) -> &Path { + self.path.as_ref() + } + + /// Reference to `Metadata` struct for the file + pub fn metadata(&self) -> &Metadata { + &self.metadata + } +} + +/// Configuration for a file or dir filter +#[derive(Debug, Clone)] +pub struct Config { + /// Set a specific read buffer size (default auto detect) + pub read_buffer_size: Option, + /// Set a specific content-type (default auto detect) + pub content_type: Option, + /// include the LastModified header in the response + pub last_modified: bool, + /// Include the Etag header in the response + pub etag: bool, + /// extra headers to add + pub headers: HeaderMap, + /// Callback to adjust the Config per request (useful for dir()) + pub callback: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + read_buffer_size: None, + content_type: None, + last_modified: true, + etag: false, + headers: Default::default(), + callback: None, + } + } +} + +impl Config { + /// Override the content_type + pub fn content_type(mut self, content_type: Option>) -> Self { + self.content_type = content_type.map(Into::into); + self + } + + /// Override the last_modified exposure + pub fn last_modified(mut self, last_modified: bool) -> Self { + self.last_modified = last_modified; + self + } + + /// Override the last_modified exposure + pub fn etag(mut self, etag: bool) -> Self { + self.etag = etag; + self + } + + /// Add additional headers + pub fn add_header(mut self, key: impl IntoHeaderName, value: HeaderValue) -> Self { + self.headers.insert(key, value); + self + } + + /// Set a callback to modification the config per request + pub fn callback(mut self, callback: Option) -> Self { + self.callback = callback; + self + } + + /// Creates a `Filter` that serves a File at the `path`. + /// + /// Does not filter out based on any information of the request. Always serves + /// the file at the exact `path` provided. Thus, this can be used to serve a + /// single file with `GET`s, but could also be used in combination with other + /// filters, such as after validating in `POST` request, wanting to return a + /// specific file as the body. + /// + /// For serving a directory, see [dir]. + /// + /// # Example + /// + /// ``` + /// // Always serves this file from the file system. + /// let route = warp::fs::config().file("/www/static/app.js"); + /// ``` + pub fn file( + self, + path: impl Into, + ) -> impl FilterClone, Error = Rejection> { + let path = Arc::new(path.into()); + let config = Arc::new(self); + let config = crate::any().map(move || config.clone()); + + crate::any() + .map(move || { + tracing::trace!("file: {:?}", path); + ArcPath(path.clone()) + }) + .and(conditionals()) + .and(config) + .and_then(file_reply) + } + + /// Creates a `Filter` that serves a directory at the base `path` joined + /// by the request path. + /// + /// This can be used to serve "static files" from a directory. By far the most + /// common pattern of serving static files is for `GET` requests, so this + /// filter automatically includes a `GET` check. + /// + /// # Example + /// + /// ``` + /// use warp::Filter; + /// + /// // Matches requests that start with `/static`, + /// // and then uses the rest of that path to lookup + /// // and serve a file from `/www/static`. + /// let route = warp::path("static") + /// .and(warp::fs::config().dir("/www/static")); + /// + /// // For example: + /// // - `GET /static/app.js` would serve the file `/www/static/app.js` + /// // - `GET /static/css/app.css` would serve the file `/www/static/css/app.css` + /// ``` + pub fn dir( + self, + path: impl Into, + ) -> impl FilterClone, Error = Rejection> { + let base = Arc::new(path.into()); + let config = Arc::new(self); + let config = crate::any().map(move || config.clone()); + + crate::get() + .or(crate::head()) + .unify() + .and(path_from_tail(base)) + .and(conditionals()) + .and(config) + .and_then(file_reply) + } +} + +/// Creates a new configuration for creating a `Filter` that serves a file or directory of static assets. +/// +/// Allows to override configuration before building the final file or dir filter. +/// +/// For serving a single file, see [Config::file] +/// For serving a directory, see [Config::dir] +pub fn config() -> Config { + Config::default() +} + /// Creates a `Filter` that serves a File at the `path`. /// /// Does not filter out based on any information of the request. Always serves @@ -39,21 +214,18 @@ use crate::reply::{Reply, Response}; /// /// For serving a directory, see [dir]. /// +/// See also [config] +/// /// # Example /// /// ``` /// // Always serves this file from the file system. +/// # #[allow(deprecated)] /// let route = warp::fs::file("/www/static/app.js"); /// ``` +#[deprecated(since = "0.3.7", note = "Use config().file(path) instead")] pub fn file(path: impl Into) -> impl FilterClone, Error = Rejection> { - let path = Arc::new(path.into()); - crate::any() - .map(move || { - tracing::trace!("file: {:?}", path); - ArcPath(path.clone()) - }) - .and(conditionals()) - .and_then(file_reply) + config().file(path) } /// Creates a `Filter` that serves a directory at the base `path` joined @@ -63,6 +235,8 @@ pub fn file(path: impl Into) -> impl FilterClone, E /// common pattern of serving static files is for `GET` requests, so this /// filter automatically includes a `GET` check. /// +/// See also [config] +/// /// # Example /// /// ``` @@ -71,6 +245,7 @@ pub fn file(path: impl Into) -> impl FilterClone, E /// // Matches requests that start with `/static`, /// // and then uses the rest of that path to lookup /// // and serve a file from `/www/static`. +/// # #[allow(deprecated)] /// let route = warp::path("static") /// .and(warp::fs::dir("/www/static")); /// @@ -78,14 +253,9 @@ pub fn file(path: impl Into) -> impl FilterClone, E /// // - `GET /static/app.js` would serve the file `/www/static/app.js` /// // - `GET /static/css/app.css` would serve the file `/www/static/css/app.css` /// ``` +#[deprecated(since = "0.3.7", note = "Use config().dir(path) instead")] pub fn dir(path: impl Into) -> impl FilterClone, Error = Rejection> { - let base = Arc::new(path.into()); - crate::get() - .or(crate::head()) - .unify() - .and(path_from_tail(base)) - .and(conditionals()) - .and_then(file_reply) + config().dir(path) } fn path_from_tail( @@ -141,6 +311,8 @@ struct Conditionals { if_unmodified_since: Option, if_range: Option, range: Option, + if_match: Option, + if_none_match: Option, } enum Cond { @@ -149,14 +321,60 @@ enum Cond { } impl Conditionals { - fn check(self, last_modified: Option) -> Cond { + fn check( + self, + config: Arc, + etag: Option<&ETag>, + last_modified: Option, + ) -> Cond { + if let Some(tag_match) = self.if_match { + let precondition = etag + .map(|tag| tag_match.precondition_passes(tag)) + .unwrap_or(false); + + tracing::trace!( + "if-match? header = {:?}, file = {:?}, result = {}", + tag_match, + etag, + precondition + ); + if !precondition { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::PRECONDITION_FAILED; + return Cond::NoBody(res); + } + } + + if let Some(tag_match) = self.if_none_match { + let precondition = etag + .map(|tag| !tag_match.precondition_passes(tag)) + // no etag means its always unmatched + .unwrap_or(false); + + tracing::trace!( + "if-none-match? header = {:?}, file = {:?}, result = {}", + tag_match, + etag, + precondition + ); + if precondition { + let mut res = Response::new(Body::empty()); + *res.status_mut() = StatusCode::NOT_MODIFIED; + res.headers_mut().typed_insert(etag.unwrap().clone()); + if let Some(cache) = config.headers.get("cache-control") { + res.headers_mut().insert("cache-control", cache.clone()); + } + return Cond::NoBody(res); + } + } + 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 {:?} = {}", + "if-unmodified-since? header = {:?}, file = {:?}, result = {}", since, last_modified, precondition @@ -169,15 +387,17 @@ impl Conditionals { } 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); + + tracing::trace!( + "if-modified-since? header = {:?}, file = {:?}, result = {}", + since, + last_modified, + unmodified + ); if unmodified { let mut res = Response::new(Body::empty()); *res.status_mut() = StatusCode::NOT_MODIFIED; @@ -186,8 +406,15 @@ impl Conditionals { } 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()); + let can_range = !if_range.is_modified(etag, last_modified.as_ref()); + + tracing::trace!( + "if-range? header = {:?}, file = {:?},{:?}, result = {}", + if_range, + etag, + last_modified, + can_range + ); if !can_range { return Cond::WithBody(None); @@ -200,15 +427,21 @@ impl Conditionals { fn conditionals() -> impl Filter, Error = Infallible> + Copy { crate::header::optional2() + .and(crate::header::optional2()) + .and(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, + |if_modified_since, if_unmodified_since, if_range, range, if_match, if_none_match| { + Conditionals { + if_modified_since, + if_unmodified_since, + if_range, + range, + if_match, + if_none_match, + } }, ) } @@ -231,7 +464,7 @@ impl File { /// use warp::{Filter, reply::Reply}; /// /// let route = warp::path("static") - /// .and(warp::fs::dir("/www/static")) + /// .and(warp::fs::config().dir("/www/static")) /// .map(|reply: warp::filters::fs::File| { /// if reply.path().ends_with("video.mp4") { /// warp::reply::with_header(reply, "Content-Type", "video/mp4").into_response() @@ -264,9 +497,10 @@ impl Reply for File { fn file_reply( path: ArcPath, conditionals: Conditionals, + config: Arc, ) -> impl Future> + Send { TkFile::open(path.clone()).then(move |res| match res { - Ok(f) => Either::Left(file_conditional(f, path, conditionals)), + Ok(f) => Either::Left(file_conditional(f, path, conditionals, config)), Err(err) => { let rej = match err.kind() { io::ErrorKind::NotFound => { @@ -305,18 +539,39 @@ fn file_conditional( f: TkFile, path: ArcPath, conditionals: Conditionals, + config: Arc, ) -> impl Future> + Send { file_metadata(f).map_ok(move |(file, meta)| { + let config = config + .callback + .and_then(|callback| callback(Context::new(&path, &meta), config.as_ref())) + .map(Arc::new) + .unwrap_or(config); let mut len = meta.len(); let modified = meta.modified().ok().map(LastModified::from); + let etag = if config.etag { + modified.and_then(|modified| { + // do a quick weak etag based on modified stamp + let modified: SystemTime = modified.into(); + let modified = modified.duration_since(SystemTime::UNIX_EPOCH); + modified + .map(|modified| format!("W/\"{:02X?}\"", modified)) + .map(|modified| modified.parse::().expect("Invalid ETag")) + .ok() + }) + } else { + None + }; - let resp = match conditionals.check(modified) { + let resp = match conditionals.check(config.clone(), etag.as_ref(), modified) { Cond::NoBody(resp) => resp, Cond::WithBody(range) => { bytes_range(range, len) .map(|(start, end)| { let sub_len = end - start; - let buf_size = optimal_buf_size(&meta); + let buf_size = config + .read_buffer_size + .unwrap_or_else(|| optimal_buf_size(&meta)); let stream = file_stream(file, buf_size, (start, end)); let body = Body::wrap_stream(stream); @@ -331,14 +586,33 @@ fn file_conditional( len = sub_len; } - let mime = mime_guess::from_path(path.as_ref()).first_or_octet_stream(); + let content_type = config.content_type.as_ref().map_or_else( + || { + ContentType::from( + mime_guess::from_path(path.as_ref()).first_or_octet_stream(), + ) + }, + |content_type| content_type.parse().expect("valid ContentType"), + ); resp.headers_mut().typed_insert(ContentLength(len)); - resp.headers_mut().typed_insert(ContentType::from(mime)); + resp.headers_mut().typed_insert(content_type); resp.headers_mut().typed_insert(AcceptRanges::bytes()); - if let Some(last_modified) = modified { - resp.headers_mut().typed_insert(last_modified); + if config.last_modified { + if let Some(last_modified) = modified { + resp.headers_mut().typed_insert(last_modified); + } + } + + if config.etag { + if let Some(etag) = etag { + resp.headers_mut().typed_insert(etag); + } + } + + for (k, v) in config.headers.iter() { + resp.headers_mut().insert(k, v.clone()); } resp @@ -497,7 +771,7 @@ unit_error! { } unit_error! { - pub(crate) FilePermissionError: "file perimission error" + pub(crate) FilePermissionError: "file permission error" } #[cfg(test)] diff --git a/tests/fs.rs b/tests/fs.rs index 4faa933d5..fdc0a7a36 100644 --- a/tests/fs.rs +++ b/tests/fs.rs @@ -5,7 +5,7 @@ use std::fs; async fn file() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::file("README.md"); + let file = warp::fs::config().file("README.md"); let req = warp::test::request(); let res = req.reply(&file).await; @@ -15,6 +15,8 @@ async fn file() { let contents = fs::read("README.md").expect("fs::read README.md"); assert_eq!(res.headers()["content-length"], contents.len().to_string()); assert_eq!(res.headers()["accept-ranges"], "bytes"); + assert!(res.headers().contains_key("last-modified")); + assert!(!res.headers().contains_key("etag")); let ct = &res.headers()["content-type"]; assert!( @@ -26,11 +28,155 @@ async fn file() { assert_eq!(res.body(), &*contents); } +#[tokio::test] +async fn file_overridden_content_type() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config() + .content_type(Some("text/plain")) + .file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + + assert_eq!(res.headers()["content-type"], "text/plain"); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn file_overridden_exclude_last_modified() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config().last_modified(false).file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + assert!(!res.headers().contains_key("last-modified")); + assert!(!res.headers().contains_key("etag")); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn file_overridden_expose_etag() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config().etag(true).file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + assert!(res.headers().contains_key("last-modified")); + assert!(res.headers().contains_key("etag")); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn file_etag_check() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config().etag(true).file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + let etag = res.headers()["etag"].clone(); + + // Make with same etag + let req = warp::test::request().header("if-none-match", etag); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 304); + + // Make with wrong etag + let req = warp::test::request().header("if-none-match", "W/\"Another\""); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); +} + +#[tokio::test] +async fn file_overridden_extra_headers() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config() + .add_header("cache-control", "private, no-store".parse().unwrap()) + .file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + assert!(res.headers().contains_key("last-modified")); + assert!(!res.headers().contains_key("etag")); + assert_eq!(res.headers()["cache-control"], "private, no-store"); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +async fn file_overridden_callback() { + let _ = pretty_env_logger::try_init(); + + let file = warp::fs::config() + .callback(Some(|_, config| { + Some( + config + .clone() + .add_header("cache-control", "private, no-store".parse().unwrap()), + ) + })) + .file("README.md"); + + let req = warp::test::request(); + let res = req.reply(&file).await; + + assert_eq!(res.status(), 200); + + let contents = fs::read("README.md").expect("fs::read README.md"); + assert_eq!(res.headers()["content-length"], contents.len().to_string()); + assert_eq!(res.headers()["accept-ranges"], "bytes"); + assert!(res.headers().contains_key("last-modified")); + assert!(!res.headers().contains_key("etag")); + assert_eq!(res.headers()["cache-control"], "private, no-store"); + + assert_eq!(res.body(), &*contents); +} + +#[tokio::test] +#[ignore = "Figure out how to test read_buff_size override"] +async fn file_overridden_read_buff_size() { + todo!("Implement this test somehow") +} + #[tokio::test] async fn dir() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/todos.rs"); let res = req.reply(&file).await; @@ -52,7 +198,7 @@ async fn dir() { async fn dir_encoded() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/todos%2ers"); let res = req.reply(&file).await; @@ -69,7 +215,7 @@ async fn dir_encoded() { async fn dir_not_found() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/definitely-not-found"); let res = req.reply(&file).await; @@ -81,7 +227,7 @@ async fn dir_not_found() { async fn dir_bad_path() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/../README.md"); let res = req.reply(&file).await; @@ -93,7 +239,7 @@ async fn dir_bad_path() { async fn dir_bad_encoded_path() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/%2E%2e/README.md"); let res = req.reply(&file).await; @@ -105,7 +251,7 @@ async fn dir_bad_encoded_path() { async fn dir_fallback_index_on_dir() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::dir("examples"); + let file = warp::fs::config().dir("examples"); let req = warp::test::request().path("/dir"); let res = req.reply(&file).await; let contents = fs::read("examples/dir/index.html").expect("fs::read"); @@ -121,7 +267,7 @@ async fn dir_fallback_index_on_dir() { async fn not_modified() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::file("README.md"); + let file = warp::fs::config().file("README.md"); let req = warp::test::request(); let body = fs::read("README.md").unwrap(); @@ -152,7 +298,7 @@ async fn not_modified() { async fn precondition() { let _ = pretty_env_logger::try_init(); - let file = warp::fs::file("README.md"); + let file = warp::fs::config().file("README.md"); let req = warp::test::request(); let res1 = req.reply(&file).await; @@ -179,7 +325,7 @@ async fn byte_ranges() { let _ = pretty_env_logger::try_init(); let contents = fs::read("README.md").expect("fs::read README.md"); - let file = warp::fs::file("README.md"); + let file = warp::fs::config().file("README.md"); let res = warp::test::request() .header("range", "bytes=100-200") @@ -235,7 +381,7 @@ async fn byte_ranges_with_excluded_file_size() { let _ = pretty_env_logger::try_init(); let contents = fs::read("README.md").expect("fs::read README.md"); - let file = warp::fs::file("README.md"); + let file = warp::fs::config().file("README.md"); // range including end of file (non-inclusive result) let res = warp::test::request()