diff --git a/test-files/subdir/foo.txt b/test-files/subdir/foo.txt new file mode 100644 index 00000000..ab0826d5 --- /dev/null +++ b/test-files/subdir/foo.txt @@ -0,0 +1 @@ +Hi from foo.txt diff --git a/tower-http/Cargo.toml b/tower-http/Cargo.toml index d4e8fa75..003a01af 100644 --- a/tower-http/Cargo.toml +++ b/tower-http/Cargo.toml @@ -50,6 +50,7 @@ tower = { version = "0.4.10", features = ["buffer", "util", "retry", "make", "ti tracing-subscriber = "0.3" uuid = { version = "1.0", features = ["v4"] } serde_json = "1.0" +rust-embed = "6.4" [features] default = [] diff --git a/tower-http/src/services/fs/backend.rs b/tower-http/src/services/fs/backend.rs new file mode 100644 index 00000000..947cb8f8 --- /dev/null +++ b/tower-http/src/services/fs/backend.rs @@ -0,0 +1,98 @@ +use futures_util::future::BoxFuture; +use std::{future::Future, io, path::Path, time::SystemTime}; +use tokio::io::{AsyncRead, AsyncSeek}; + +// TODO(david): try and rewrite this using async-trait to see if that matters much +// currently this requires GATs which is maybe pushing tower-http's MSRV a bit? +// +// async-trait is unfortunate because it requires syn+quote and requiring allocations +// futures is unfortunate if the data is in the binary, as for rust-embed + +// TODO(david): implement a backend using rust-embed to prove that its possible + +pub trait Backend: Clone + Send + Sync + 'static { + type File: File; + type Metadata: Metadata; + + type OpenFuture: Future> + Send; + type MetadataFuture: Future> + Send; + + fn open(&self, path: A) -> Self::OpenFuture + where + A: AsRef; + + fn metadata(&self, path: A) -> Self::MetadataFuture + where + A: AsRef; +} + +pub trait Metadata: Send + 'static { + fn is_dir(&self) -> bool; + + fn modified(&self) -> io::Result; + + fn len(&self) -> u64; +} + +pub trait File: AsyncRead + AsyncSeek + Unpin + Send + Sync { + type Metadata: Metadata; + type MetadataFuture<'a>: Future> + Send + where + Self: 'a; + + fn metadata(&self) -> Self::MetadataFuture<'_>; +} + +#[derive(Default, Debug, Clone)] +#[non_exhaustive] +pub struct TokioBackend; + +impl Backend for TokioBackend { + type File = tokio::fs::File; + type Metadata = std::fs::Metadata; + + type OpenFuture = BoxFuture<'static, io::Result>; + type MetadataFuture = BoxFuture<'static, io::Result>; + + fn open(&self, path: A) -> Self::OpenFuture + where + A: AsRef, + { + let path = path.as_ref().to_owned(); + Box::pin(tokio::fs::File::open(path)) + } + + fn metadata(&self, path: A) -> Self::MetadataFuture + where + A: AsRef, + { + let path = path.as_ref().to_owned(); + Box::pin(tokio::fs::metadata(path)) + } +} + +impl File for tokio::fs::File { + type Metadata = std::fs::Metadata; + type MetadataFuture<'a> = BoxFuture<'a, io::Result>; + + fn metadata(&self) -> Self::MetadataFuture<'_> { + Box::pin(self.metadata()) + } +} + +impl Metadata for std::fs::Metadata { + #[inline] + fn is_dir(&self) -> bool { + self.is_dir() + } + + #[inline] + fn modified(&self) -> io::Result { + self.modified() + } + + #[inline] + fn len(&self) -> u64 { + self.len() + } +} diff --git a/tower-http/src/services/fs/mod.rs b/tower-http/src/services/fs/mod.rs index ce6ef463..c5c26894 100644 --- a/tower-http/src/services/fs/mod.rs +++ b/tower-http/src/services/fs/mod.rs @@ -13,10 +13,12 @@ use std::{ use tokio::io::{AsyncRead, AsyncReadExt, Take}; use tokio_util::io::ReaderStream; +mod backend; mod serve_dir; mod serve_file; pub use self::{ + backend::{Backend, File, Metadata}, serve_dir::{ future::ResponseFuture as ServeFileSystemResponseFuture, DefaultServeDirFallback, diff --git a/tower-http/src/services/fs/serve_dir/future.rs b/tower-http/src/services/fs/serve_dir/future.rs index 36b35039..78fa5def 100644 --- a/tower-http/src/services/fs/serve_dir/future.rs +++ b/tower-http/src/services/fs/serve_dir/future.rs @@ -1,8 +1,11 @@ use super::{ open_file::{FileOpened, FileRequestExtent, OpenFileOutput}, - DefaultServeDirFallback, ResponseBody, + ResponseBody, +}; +use crate::{ + services::fs::{AsyncReadBody, Backend, Metadata as _}, + BoxError, }; -use crate::{services::fs::AsyncReadBody, BoxError}; use bytes::Bytes; use futures_util::{ future::{BoxFuture, FutureExt, TryFutureExt}, @@ -25,15 +28,15 @@ use tower_service::Service; pin_project! { /// Response future of [`ServeDir::try_call`]. - pub struct ResponseFuture { + pub struct ResponseFuture { #[pin] - pub(super) inner: ResponseFutureInner, + pub(super) inner: ResponseFutureInner, } } -impl ResponseFuture { +impl ResponseFuture { pub(super) fn open_file_future( - future: BoxFuture<'static, io::Result>, + future: BoxFuture<'static, io::Result>>, fallback_and_request: Option<(F, Request)>, ) -> Self { Self { @@ -61,10 +64,10 @@ impl ResponseFuture { pin_project! { #[project = ResponseFutureInnerProj] - pub(super) enum ResponseFutureInner { + pub(super) enum ResponseFutureInner { OpenFileFuture { #[pin] - future: BoxFuture<'static, io::Result>, + future: BoxFuture<'static, io::Result>>, fallback_and_request: Option<(F, Request)>, }, FallbackFuture { @@ -77,12 +80,13 @@ pin_project! { } } -impl Future for ResponseFuture +impl Future for ResponseFuture where F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, ResBody: http_body::Body + Send + 'static, ResBody::Error: Into>, + B: Backend, { type Output = io::Result>; @@ -176,15 +180,16 @@ fn not_found() -> Response { response_with_status(StatusCode::NOT_FOUND) } -pub(super) fn call_fallback( +pub(super) fn call_fallback( fallback: &mut F, - req: Request, -) -> ResponseFutureInner + req: Request, +) -> ResponseFutureInner where - F: Service, Response = Response, Error = Infallible> + Clone, + F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, FResBody: http_body::Body + Send + 'static, FResBody::Error: Into, + B: Backend, { let future = fallback .call(req) @@ -204,7 +209,10 @@ where ResponseFutureInner::FallbackFuture { future } } -fn build_response(output: FileOpened) -> Response { +fn build_response(output: FileOpened) -> Response +where + B: Backend, +{ let (maybe_file, size) = match output.extent { FileRequestExtent::Full(file, meta) => (Some(file), meta.len()), FileRequestExtent::Head(meta) => (None, meta.len()), diff --git a/tower-http/src/services/fs/serve_dir/mod.rs b/tower-http/src/services/fs/serve_dir/mod.rs index 6c869d19..950609a1 100644 --- a/tower-http/src/services/fs/serve_dir/mod.rs +++ b/tower-http/src/services/fs/serve_dir/mod.rs @@ -8,14 +8,19 @@ use futures_util::FutureExt; use http::{header, HeaderValue, Method, Request, Response, StatusCode}; use http_body::{combinators::UnsyncBoxBody, Body, Empty}; use percent_encoding::percent_decode; +use pin_project_lite::pin_project; use std::{ convert::Infallible, + future::Future, io, + marker::PhantomData, path::{Component, Path, PathBuf}, task::{Context, Poll}, }; use tower_service::Service; +use super::{backend::TokioBackend, Backend}; + pub(crate) mod future; mod headers; mod open_file; @@ -56,7 +61,7 @@ const DEFAULT_CAPACITY: usize = 65536; /// # }; /// ``` #[derive(Clone, Debug)] -pub struct ServeDir { +pub struct ServeDir, B = TokioBackend> { base: PathBuf, buf_chunk_size: usize, precompressed_variants: Option, @@ -65,9 +70,10 @@ pub struct ServeDir { variant: ServeVariant, fallback: Option, call_fallback_on_method_not_allowed: bool, + backend: B, } -impl ServeDir { +impl ServeDir, TokioBackend> { /// Create a new [`ServeDir`]. pub fn new

(path: P) -> Self where @@ -85,6 +91,7 @@ impl ServeDir { }, fallback: None, call_fallback_on_method_not_allowed: false, + backend: TokioBackend, } } @@ -99,11 +106,12 @@ impl ServeDir { variant: ServeVariant::SingleFile { mime }, fallback: None, call_fallback_on_method_not_allowed: false, + backend: TokioBackend, } } } -impl ServeDir { +impl ServeDir { /// If the requested path is a directory append `index.html`. /// /// This is useful for static sites. @@ -207,7 +215,7 @@ impl ServeDir { /// .expect("server error"); /// # }; /// ``` - pub fn fallback(self, new_fallback: F2) -> ServeDir { + pub fn fallback(self, new_fallback: F2) -> ServeDir { ServeDir { base: self.base, buf_chunk_size: self.buf_chunk_size, @@ -215,6 +223,7 @@ impl ServeDir { variant: self.variant, fallback: Some(new_fallback), call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed, + backend: self.backend, } } @@ -244,7 +253,7 @@ impl ServeDir { /// ``` /// /// Setups like this are often found in single page applications. - pub fn not_found_service(self, new_fallback: F2) -> ServeDir> { + pub fn not_found_service(self, new_fallback: F2) -> ServeDir, B> { self.fallback(SetStatus::new(new_fallback, StatusCode::NOT_FOUND)) } @@ -256,6 +265,22 @@ impl ServeDir { self } + /// TODO(david): docs + pub fn backend(self, new_backend: B2) -> ServeDir + where + B2: Backend, + { + ServeDir { + base: self.base, + buf_chunk_size: self.buf_chunk_size, + precompressed_variants: self.precompressed_variants, + variant: self.variant, + fallback: self.fallback, + call_fallback_on_method_not_allowed: self.call_fallback_on_method_not_allowed, + backend: new_backend, + } + } + /// Call the service and get a future that contains any `std::io::Error` that might have /// happened. /// @@ -322,12 +347,13 @@ impl ServeDir { pub fn try_call( &mut self, req: Request, - ) -> ResponseFuture + ) -> ResponseFuture where F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, FResBody: http_body::Body + Send + 'static, FResBody::Error: Into>, + B: Backend, { if req.method() != Method::GET && req.method() != Method::HEAD { if self.call_fallback_on_method_not_allowed { @@ -395,22 +421,24 @@ impl ServeDir { negotiated_encodings, range_header, buf_chunk_size, + self.backend.clone(), )); ResponseFuture::open_file_future(open_file_future, fallback_and_request) } } -impl Service> for ServeDir +impl Service> for ServeDir where F: Service, Response = Response, Error = Infallible> + Clone, F::Future: Send + 'static, FResBody: http_body::Body + Send + 'static, FResBody::Error: Into>, + B: Backend, { type Response = Response; type Error = Infallible; - type Future = InfallibleResponseFuture; + type Future = InfallibleResponseFuture; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { @@ -438,17 +466,37 @@ where Ok(response) } as _); - InfallibleResponseFuture::new(future) + InfallibleResponseFuture { future } } } -opaque_future! { +pin_project! { /// Response future of [`ServeDir`]. - pub type InfallibleResponseFuture = - futures_util::future::Map< - ResponseFuture, + pub struct InfallibleResponseFuture { + #[pin] + pub(crate) future: futures_util::future::Map< + ResponseFuture, fn(Result, io::Error>) -> Result, Infallible>, - >; + >, + } +} + +impl Future for InfallibleResponseFuture +where + F: Service, Response = Response, Error = Infallible> + Clone, + F::Future: Send + 'static, + FResBody: http_body::Body + Send + 'static, + FResBody::Error: Into>, + B: Backend, +{ + type Output = , + fn(Result, io::Error>) -> Result, Infallible>, + > as Future>::Output; + + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().future.poll(cx) + } } // Allow the ServeDir service to be used in the ServeFile service @@ -507,16 +555,23 @@ opaque_body! { } /// The default fallback service used with [`ServeDir`]. -#[derive(Debug, Clone, Copy)] -pub struct DefaultServeDirFallback(Infallible); +#[derive(Debug, Copy)] +pub struct DefaultServeDirFallback(Infallible, PhantomData B::Metadata>); + +impl Clone for DefaultServeDirFallback { + fn clone(&self) -> Self { + Self(self.0, self.1) + } +} -impl Service> for DefaultServeDirFallback +impl Service> for DefaultServeDirFallback where ReqBody: Send + 'static, + B: Backend, { type Response = Response; type Error = Infallible; - type Future = InfallibleResponseFuture; + type Future = InfallibleResponseFuture; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { match self.0 {} diff --git a/tower-http/src/services/fs/serve_dir/open_file.rs b/tower-http/src/services/fs/serve_dir/open_file.rs index a24aa088..71790918 100644 --- a/tower-http/src/services/fs/serve_dir/open_file.rs +++ b/tower-http/src/services/fs/serve_dir/open_file.rs @@ -2,30 +2,32 @@ use super::{ headers::{IfModifiedSince, IfUnmodifiedSince, LastModified}, ServeVariant, }; -use crate::content_encoding::{Encoding, QValue}; +use crate::{ + content_encoding::{Encoding, QValue}, + services::fs::{backend::File, Backend, Metadata as _}, +}; use bytes::Bytes; use http::{header, HeaderValue, Method, Request, Uri}; use http_body::Empty; use http_range_header::RangeUnsatisfiableError; use std::{ ffi::OsStr, - fs::Metadata, io::{self, SeekFrom}, ops::RangeInclusive, path::{Path, PathBuf}, }; -use tokio::{fs::File, io::AsyncSeekExt}; +use tokio::io::AsyncSeekExt; -pub(super) enum OpenFileOutput { - FileOpened(Box), +pub(super) enum OpenFileOutput { + FileOpened(Box>), Redirect { location: HeaderValue }, FileNotFound, PreconditionFailed, NotModified, } -pub(super) struct FileOpened { - pub(super) extent: FileRequestExtent, +pub(super) struct FileOpened { + pub(super) extent: FileRequestExtent, pub(super) chunk_size: usize, pub(super) mime_header_value: HeaderValue, pub(super) maybe_encoding: Option, @@ -33,19 +35,23 @@ pub(super) struct FileOpened { pub(super) last_modified: Option, } -pub(super) enum FileRequestExtent { - Full(File, Metadata), - Head(Metadata), +pub(super) enum FileRequestExtent { + Full(B::File, B::Metadata), + Head(B::Metadata), } -pub(super) async fn open_file( +pub(super) async fn open_file( variant: ServeVariant, mut path_to_file: PathBuf, req: Request>, negotiated_encodings: Vec<(Encoding, QValue)>, range_header: Option, buf_chunk_size: usize, -) -> io::Result { + backend: B, +) -> io::Result> +where + B: Backend, +{ let if_unmodified_since = req .headers() .get(header::IF_UNMODIFIED_SINCE) @@ -67,6 +73,7 @@ pub(super) async fn open_file( &mut path_to_file, req.uri(), append_index_html_on_directories, + &backend, ) .await { @@ -86,7 +93,7 @@ pub(super) async fn open_file( if req.method() == Method::HEAD { let (meta, maybe_encoding) = - file_metadata_with_fallback(path_to_file, negotiated_encodings).await?; + file_metadata_with_fallback(path_to_file, negotiated_encodings, &backend).await?; let last_modified = meta.modified().ok().map(LastModified::from); if let Some(output) = check_modified_headers( @@ -109,7 +116,7 @@ pub(super) async fn open_file( }))) } else { let (mut file, maybe_encoding) = - open_file_with_fallback(path_to_file, negotiated_encodings).await?; + open_file_with_fallback(path_to_file, negotiated_encodings, &backend).await?; let meta = file.metadata().await?; let last_modified = meta.modified().ok().map(LastModified::from); if let Some(output) = check_modified_headers( @@ -140,11 +147,11 @@ pub(super) async fn open_file( } } -fn check_modified_headers( +fn check_modified_headers( modified: Option<&LastModified>, if_unmodified_since: Option, if_modified_since: Option, -) -> Option { +) -> Option> { if let Some(since) = if_unmodified_since { let precondition = modified .as_ref() @@ -199,14 +206,15 @@ fn 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( +async fn open_file_with_fallback( mut path: PathBuf, mut negotiated_encoding: Vec<(Encoding, QValue)>, -) -> io::Result<(File, Option)> { + backend: &B, +) -> io::Result<(B::File, Option)> { 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) { + match (backend.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) @@ -226,14 +234,18 @@ async fn open_file_with_fallback( // 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( +async fn file_metadata_with_fallback( mut path: PathBuf, mut negotiated_encoding: Vec<(Encoding, QValue)>, -) -> io::Result<(Metadata, Option)> { + backend: &B, +) -> io::Result<(B::Metadata, Option)> +where + B: Backend, +{ 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) { + match (backend.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) @@ -250,20 +262,24 @@ async fn file_metadata_with_fallback( Ok((file, encoding)) } -async fn maybe_redirect_or_append_path( +async fn maybe_redirect_or_append_path( path_to_file: &mut PathBuf, uri: &Uri, append_index_html_on_directories: bool, -) -> Option { + backend: &B, +) -> Option> +where + B: Backend, +{ if !uri.path().ends_with('/') { - if is_dir(path_to_file).await { + if is_dir(path_to_file, backend).await { let location = HeaderValue::from_str(&append_slash_on_path(uri.clone()).to_string()).unwrap(); Some(OpenFileOutput::Redirect { location }) } else { None } - } else if is_dir(path_to_file).await { + } else if is_dir(path_to_file, backend).await { if append_index_html_on_directories { path_to_file.push("index.html"); None @@ -285,8 +301,12 @@ fn try_parse_range( }) } -async fn is_dir(path_to_file: &Path) -> bool { - tokio::fs::metadata(path_to_file) +async fn is_dir(path_to_file: &Path, backend: &B) -> bool +where + B: Backend, +{ + backend + .metadata(path_to_file) .await .map_or(false, |meta_data| meta_data.is_dir()) } diff --git a/tower-http/src/services/fs/serve_dir/tests.rs b/tower-http/src/services/fs/serve_dir/tests.rs index acc16ec4..3e1e55d6 100644 --- a/tower-http/src/services/fs/serve_dir/tests.rs +++ b/tower-http/src/services/fs/serve_dir/tests.rs @@ -1,3 +1,4 @@ +use crate::services::fs::{self, Backend}; use crate::services::{ServeDir, ServeFile}; use brotli::BrotliDecompress; use bytes::Bytes; @@ -7,8 +8,16 @@ use http::{header, Method, Response}; use http::{Request, StatusCode}; use http_body::Body as HttpBody; use hyper::Body; +use std::borrow::Cow; use std::convert::Infallible; -use std::io::{self, Read}; +use std::future::{ready, Ready}; +use std::io::{self, Cursor, Read}; +use std::path::Path; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::SystemTime; +use tokio::io::{AsyncRead, AsyncSeek, ReadBuf}; use tower::{service_fn, ServiceExt}; #[tokio::test] @@ -711,3 +720,146 @@ async fn calls_fallback_on_invalid_paths() { assert_eq!(res.headers()["from-fallback"], "1"); } + +#[tokio::test] +async fn custom_backend() { + #[derive(Clone, rust_embed::RustEmbed)] + #[folder = "../test-files"] + struct RustEmbedFiles; + + impl RustEmbedFiles { + fn get_file(&self, path: &Path) -> io::Result { + // TODO(david): we have to handle directories here which rust-embed does not + + for file in Self::iter() { + dbg!(&file); + } + + if path == Path::new("./.") { + todo!("bingo"); + } + + for filename in Self::iter() { + dbg!(filename); + } + + if let Some(file) = Self::get(path.to_str().unwrap()) { + Ok(file) + } else { + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("{} not found", path.display()), + )) + } + } + } + + impl Backend for RustEmbedFiles { + type File = RustEmbedFile; + type Metadata = RustEmbedMetadata; + + type OpenFuture = Ready>; + type MetadataFuture = Ready>; + + fn open(&self, path: A) -> Self::OpenFuture + where + A: AsRef, + { + let path = path.as_ref(); + + let rust_embed::EmbeddedFile { data, metadata } = match self.get_file(path) { + Ok(file) => file, + Err(err) => return ready(Err(err)), + }; + + ready(Ok(RustEmbedFile { + metadata: Arc::new(metadata), + len: data.len() as u64, + cursor: Cursor::new(data), + })) + } + + fn metadata(&self, path: A) -> Self::MetadataFuture + where + A: AsRef, + { + let rust_embed::EmbeddedFile { metadata, data } = match self.get_file(path.as_ref()) { + Ok(file) => file, + Err(err) => return ready(Err(err)), + }; + + ready(Ok(RustEmbedMetadata { + metadata: Arc::new(metadata), + len: data.len() as u64, + })) + } + } + + struct RustEmbedFile { + metadata: Arc, + cursor: std::io::Cursor>, + len: u64, + } + + impl AsyncRead for RustEmbedFile { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.cursor).poll_read(cx, buf) + } + } + + impl AsyncSeek for RustEmbedFile { + fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { + unimplemented!() + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + unimplemented!() + } + } + + impl fs::File for RustEmbedFile { + type Metadata = RustEmbedMetadata; + type MetadataFuture<'a> = Ready>; + + fn metadata(&self) -> Self::MetadataFuture<'_> { + ready(Ok(RustEmbedMetadata { + metadata: Arc::clone(&self.metadata), + len: self.len, + })) + } + } + + struct RustEmbedMetadata { + metadata: Arc, + len: u64, + } + + impl fs::Metadata for RustEmbedMetadata { + fn is_dir(&self) -> bool { + false + } + + fn modified(&self) -> io::Result { + todo!() + } + + fn len(&self) -> u64 { + self.len + } + } + + let svc = ServeDir::new(".").backend(RustEmbedFiles); + + let req = Request::builder().uri("/").body(Body::empty()).unwrap(); + + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + + let body = body_into_text(res.into_body()).await; + assert_eq!(body, "HTML!\n"); +} diff --git a/tower-http/src/services/fs/serve_file.rs b/tower-http/src/services/fs/serve_file.rs index ede79936..c5f3fd3b 100644 --- a/tower-http/src/services/fs/serve_file.rs +++ b/tower-http/src/services/fs/serve_file.rs @@ -1,9 +1,11 @@ //! Service that serves a file. -use super::ServeDir; -use http::{HeaderValue, Request}; +use super::{backend::TokioBackend, Backend, DefaultServeDirFallback, ServeDir}; +use bytes::Bytes; +use http::{HeaderValue, Request, Response}; use mime::Mime; use std::{ + convert::Infallible, path::Path, task::{Context, Poll}, }; @@ -11,10 +13,10 @@ use tower_service::Service; /// Service that serves a file. #[derive(Clone, Debug)] -pub struct ServeFile(ServeDir); +pub struct ServeFile, B = TokioBackend>(ServeDir); // Note that this is just a special case of ServeDir -impl ServeFile { +impl ServeFile, TokioBackend> { /// Create a new [`ServeFile`]. /// /// The `Content-Type` will be guessed from the file extension. @@ -41,7 +43,9 @@ impl ServeFile { let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value"); Self(ServeDir::new_single_file(path, mime)) } +} +impl ServeFile { /// Informs the service that it should also look for a precompressed gzip /// version of the file. /// @@ -91,28 +95,48 @@ impl ServeFile { Self(self.0.with_buf_chunk_size(chunk_size)) } + /// TODO(david): docs + pub fn backend(self, new_backend: B2) -> ServeFile + where + B2: Backend, + { + ServeFile(self.0.backend(new_backend)) + } +} + +impl ServeFile { /// Call the service and get a future that contains any `std::io::Error` that might have /// happened. /// /// See [`ServeDir::try_call`] for more details. - pub fn try_call( + pub fn try_call( &mut self, req: Request, - ) -> super::serve_dir::future::ResponseFuture + ) -> super::serve_dir::future::ResponseFuture where ReqBody: Send + 'static, + F: Service, Response = Response, Error = Infallible> + Clone, + F::Future: Send + 'static, + FResBody: http_body::Body + Send + 'static, + FResBody::Error: Into>, + B: Backend, { self.0.try_call(req) } } -impl Service> for ServeFile +impl Service> for ServeFile where ReqBody: Send + 'static, + F: Service, Response = Response, Error = Infallible> + Clone, + F::Future: Send + 'static, + FResBody: http_body::Body + Send + 'static, + FResBody::Error: Into>, + B: Backend, { - type Error = >>::Error; - type Response = >>::Response; - type Future = >>::Future; + type Error = as Service>>::Error; + type Response = as Service>>::Response; + type Future = as Service>>::Future; #[inline] fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> {