mirror of
https://github.com/tower-rs/tower-http.git
synced 2026-03-23 15:10:18 +00:00
Make ServieFile specialized version of ServeDir (#187)
* Make ServieFile specialized version of ServeDir * Add missing test for index.html redirection * Fix tiny doc error * Update changelog to include breaking change in service types
This commit is contained in:
1
test-files/index.html
Normal file
1
test-files/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<b>HTML!</b>
|
||||
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
# Unreleased
|
||||
|
||||
- `ServeDir` and `ServeFile`: Changed service types, both now return `ServeFileSystemResponseBody` and `ServeFileSystemResponseFuture` ([#187])
|
||||
- `AddAuthorization`, `InFlightRequests`, `SetRequestHeader`, `SetResponseHeader`, `AddExtension`, `MapRequestBody` and `MapResponseBody`
|
||||
now requires underlying service to use `http::Request<B>` and `http::Response<U>` as request and responses ([#182])
|
||||
- `ServeDir` and `ServeFile`: Ability to serve precompressed files ([#156])
|
||||
@@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[#170]: https://github.com/tower-rs/tower-http/pull/170
|
||||
[#172]: https://github.com/tower-rs/tower-http/pull/172
|
||||
[#182]: https://github.com/tower-rs/tower-http/pull/182
|
||||
[#187]: https://github.com/tower-rs/tower-http/pull/187
|
||||
|
||||
# 0.1.2 (November 13, 2021)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use http::{HeaderMap, Response, StatusCode};
|
||||
use http_body::{combinators::BoxBody, Body, Empty};
|
||||
use pin_project_lite::pin_project;
|
||||
use std::fs::Metadata;
|
||||
use std::{ffi::OsStr, future::Future, path::PathBuf};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
@@ -27,11 +27,12 @@ use crate::content_encoding::{Encoding, SupportedEncodings};
|
||||
|
||||
pub use self::{
|
||||
serve_dir::{
|
||||
ResponseBody as ServeDirResponseBody, ResponseFuture as ServeDirResponseFuture, ServeDir,
|
||||
},
|
||||
serve_file::{
|
||||
ResponseBody as ServeFileResponseBody, ResponseFuture as ServeFileResponseFuture, ServeFile,
|
||||
// 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)]
|
||||
@@ -65,9 +66,6 @@ impl SupportedEncodings for PrecompressedVariants {
|
||||
}
|
||||
}
|
||||
|
||||
type FileFuture =
|
||||
Pin<Box<dyn Future<Output = io::Result<(File, Option<Encoding>)>> + Send + Sync + 'static>>;
|
||||
|
||||
// Returns the preferred_encoding encoding and modifies the path extension
|
||||
// to the corresponding file extension for the encoding.
|
||||
fn preferred_encoding(
|
||||
|
||||
@@ -58,14 +58,14 @@ use tower_service::Service;
|
||||
/// [`and_then`](tower::ServiceBuilder::and_then) to change the response:
|
||||
///
|
||||
/// ```
|
||||
/// use tower_http::services::fs::{ServeDir, ServeDirResponseBody};
|
||||
/// use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
/// use tower::ServiceBuilder;
|
||||
/// use http::{StatusCode, Response};
|
||||
/// use http_body::{Body as _, Full};
|
||||
/// use std::io;
|
||||
///
|
||||
/// let service = ServiceBuilder::new()
|
||||
/// .and_then(|response: Response<ServeDirResponseBody>| async move {
|
||||
/// .and_then(|response: Response<ServeFileSystemResponseBody>| async move {
|
||||
/// let response = if response.status() == StatusCode::NOT_FOUND {
|
||||
/// let body = Full::from("Not Found")
|
||||
/// .map_err(|err| match err {})
|
||||
@@ -92,9 +92,53 @@ use tower_service::Service;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServeDir {
|
||||
base: PathBuf,
|
||||
append_index_html_on_directories: bool,
|
||||
buf_chunk_size: usize,
|
||||
precompressed_variants: Option<PrecompressedVariants>,
|
||||
// This is used to specialise implementation for
|
||||
// single files
|
||||
variant: ServeVariant,
|
||||
}
|
||||
|
||||
// Allow the ServeDir service to be used in the ServeFile service
|
||||
// with almost no overhead
|
||||
#[derive(Clone, Debug)]
|
||||
enum ServeVariant {
|
||||
Directory {
|
||||
append_index_html_on_directories: bool,
|
||||
},
|
||||
SingleFile {
|
||||
mime: HeaderValue,
|
||||
},
|
||||
}
|
||||
|
||||
impl ServeVariant {
|
||||
fn full_path(&self, base_path: &Path, requested_path: &str) -> Option<PathBuf> {
|
||||
match self {
|
||||
ServeVariant::Directory {
|
||||
append_index_html_on_directories: _,
|
||||
} => {
|
||||
let full_path = build_and_validate_path(base_path, requested_path)?;
|
||||
Some(full_path)
|
||||
}
|
||||
ServeVariant::SingleFile { mime: _ } => Some(base_path.to_path_buf()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_and_validate_path(base_path: &Path, requested_path: &str) -> Option<PathBuf> {
|
||||
// build and validate the path
|
||||
let path = requested_path.trim_start_matches('/');
|
||||
|
||||
let path_decoded = percent_decode(path.as_ref()).decode_utf8().ok()?;
|
||||
|
||||
let mut full_path = base_path.to_path_buf();
|
||||
for seg in path_decoded.split('/') {
|
||||
if seg.starts_with("..") || seg.contains('\\') {
|
||||
return None;
|
||||
}
|
||||
full_path.push(seg);
|
||||
}
|
||||
Some(full_path)
|
||||
}
|
||||
|
||||
impl ServeDir {
|
||||
@@ -105,9 +149,20 @@ impl ServeDir {
|
||||
|
||||
Self {
|
||||
base,
|
||||
append_index_html_on_directories: true,
|
||||
buf_chunk_size: DEFAULT_CAPACITY,
|
||||
precompressed_variants: None,
|
||||
variant: ServeVariant::Directory {
|
||||
append_index_html_on_directories: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_single_file<P: AsRef<Path>>(path: P, mime: HeaderValue) -> Self {
|
||||
Self {
|
||||
base: path.as_ref().to_owned(),
|
||||
buf_chunk_size: DEFAULT_CAPACITY,
|
||||
precompressed_variants: None,
|
||||
variant: ServeVariant::SingleFile { mime },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +172,15 @@ impl ServeDir {
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
pub fn append_index_html_on_directories(mut self, append: bool) -> Self {
|
||||
self.append_index_html_on_directories = append;
|
||||
self
|
||||
match &mut self.variant {
|
||||
ServeVariant::Directory {
|
||||
append_index_html_on_directories,
|
||||
} => {
|
||||
*append_index_html_on_directories = append;
|
||||
self
|
||||
}
|
||||
ServeVariant::SingleFile { mime: _ } => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a specific read buffer chunk size.
|
||||
@@ -181,6 +243,29 @@ impl ServeDir {
|
||||
}
|
||||
}
|
||||
|
||||
async fn maybe_redirect_or_append_path(
|
||||
full_path: &mut PathBuf,
|
||||
uri: Uri,
|
||||
append_index_html_on_directories: bool,
|
||||
) -> Option<Output> {
|
||||
if !uri.path().ends_with('/') {
|
||||
if is_dir(full_path).await {
|
||||
let location = HeaderValue::from_str(&append_slash_on_path(uri).to_string()).unwrap();
|
||||
return Some(Output::Redirect(location));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else if is_dir(full_path).await {
|
||||
if append_index_html_on_directories {
|
||||
full_path.push("index.html");
|
||||
return None;
|
||||
} else {
|
||||
return Some(Output::NotFound);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl<ReqBody> Service<Request<ReqBody>> for ServeDir {
|
||||
type Response = Response<ResponseBody>;
|
||||
type Error = io::Error;
|
||||
@@ -192,29 +277,15 @@ impl<ReqBody> Service<Request<ReqBody>> for ServeDir {
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
// build and validate the path
|
||||
let path = req.uri().path();
|
||||
let path = path.trim_start_matches('/');
|
||||
|
||||
let path_decoded = if let Ok(decoded_utf8) = percent_decode(path.as_ref()).decode_utf8() {
|
||||
decoded_utf8
|
||||
} else {
|
||||
return ResponseFuture {
|
||||
inner: Inner::Invalid,
|
||||
};
|
||||
};
|
||||
|
||||
let mut full_path = self.base.clone();
|
||||
for seg in path_decoded.split('/') {
|
||||
if seg.starts_with("..") || seg.contains('\\') {
|
||||
let mut full_path = match self.variant.full_path(&self.base, req.uri().path()) {
|
||||
Some(full_path) => full_path,
|
||||
None => {
|
||||
return ResponseFuture {
|
||||
inner: Inner::Invalid,
|
||||
};
|
||||
}
|
||||
}
|
||||
full_path.push(seg);
|
||||
}
|
||||
};
|
||||
|
||||
let append_index_html_on_directories = self.append_index_html_on_directories;
|
||||
let buf_chunk_size = self.buf_chunk_size;
|
||||
let uri = req.uri().clone();
|
||||
|
||||
@@ -226,29 +297,35 @@ impl<ReqBody> Service<Request<ReqBody>> for ServeDir {
|
||||
);
|
||||
|
||||
let request_method = req.method().clone();
|
||||
let variant = self.variant.clone();
|
||||
|
||||
let open_file_future = Box::pin(async move {
|
||||
if !uri.path().ends_with('/') {
|
||||
if is_dir(&full_path).await {
|
||||
let location =
|
||||
HeaderValue::from_str(&append_slash_on_path(uri).to_string()).unwrap();
|
||||
return Ok(Output::Redirect(location));
|
||||
let mime = match variant {
|
||||
ServeVariant::Directory {
|
||||
append_index_html_on_directories,
|
||||
} => {
|
||||
// Might already at this point know a redirect or not found result should be
|
||||
// returned which corresponds to a Some(output). Otherwise the path might be
|
||||
// modified and proceed to the open file/metadata future.
|
||||
if let Some(output) = maybe_redirect_or_append_path(
|
||||
&mut full_path,
|
||||
uri,
|
||||
append_index_html_on_directories,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(output);
|
||||
}
|
||||
let guess = mime_guess::from_path(&full_path);
|
||||
guess
|
||||
.first_raw()
|
||||
.map(|mime| HeaderValue::from_static(mime))
|
||||
.unwrap_or_else(|| {
|
||||
HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
|
||||
})
|
||||
}
|
||||
} else if is_dir(&full_path).await {
|
||||
if append_index_html_on_directories {
|
||||
full_path.push("index.html");
|
||||
} else {
|
||||
return Ok(Output::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
let guess = mime_guess::from_path(&full_path);
|
||||
let mime = guess
|
||||
.first_raw()
|
||||
.map(|mime| HeaderValue::from_static(mime))
|
||||
.unwrap_or_else(|| {
|
||||
HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
|
||||
});
|
||||
ServeVariant::SingleFile { mime } => mime,
|
||||
};
|
||||
|
||||
match request_method {
|
||||
Method::HEAD => {
|
||||
@@ -399,7 +476,7 @@ fn empty_body() -> ResponseBody {
|
||||
}
|
||||
|
||||
opaque_body! {
|
||||
/// Response body for [`ServeDir`].
|
||||
/// Response body for [`ServeDir`] and [`ServeFile`].
|
||||
pub type ResponseBody = BoxBody<Bytes, io::Error>;
|
||||
}
|
||||
|
||||
@@ -435,6 +512,20 @@ mod tests {
|
||||
assert_eq!(body, contents);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_with_index() {
|
||||
let svc = ServeDir::new("../test-files");
|
||||
|
||||
let req = Request::new(Body::empty());
|
||||
let res = svc.oneshot(req).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers()[header::CONTENT_TYPE], "text/html");
|
||||
|
||||
let body = body_into_text(res.into_body()).await;
|
||||
assert_eq!(body, "<b>HTML!</b>\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_request() {
|
||||
let svc = ServeDir::new("../test-files");
|
||||
|
||||
@@ -1,42 +1,24 @@
|
||||
//! Service that serves a file.
|
||||
|
||||
use super::{
|
||||
file_metadata_with_fallback, open_file_with_fallback, AsyncReadBody, FileFuture,
|
||||
PrecompressedVariants,
|
||||
};
|
||||
use crate::{
|
||||
content_encoding::{encodings, Encoding},
|
||||
services::fs::DEFAULT_CAPACITY,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use http::{header, method::Method, HeaderValue, Request, Response};
|
||||
use http_body::{combinators::BoxBody, Body};
|
||||
use super::ServeDir;
|
||||
use http::{HeaderValue, Request};
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
fs::Metadata,
|
||||
future::Future,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
path::Path,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower_service::Service;
|
||||
|
||||
/// Service that serves a file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServeFile {
|
||||
path: PathBuf,
|
||||
mime: HeaderValue,
|
||||
buf_chunk_size: usize,
|
||||
precompressed_variants: Option<PrecompressedVariants>,
|
||||
}
|
||||
|
||||
pub struct ServeFile(ServeDir);
|
||||
// Note that this is just a special case of ServeDir
|
||||
impl ServeFile {
|
||||
/// Create a new [`ServeFile`].
|
||||
///
|
||||
/// The `Content-Type` will be guessed from the file extension.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
let guess = mime_guess::from_path(&path);
|
||||
let guess = mime_guess::from_path(path.as_ref());
|
||||
let mime = guess
|
||||
.first_raw()
|
||||
.map(|mime| HeaderValue::from_static(mime))
|
||||
@@ -44,14 +26,7 @@ impl ServeFile {
|
||||
HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap()
|
||||
});
|
||||
|
||||
let path = path.as_ref().to_owned();
|
||||
|
||||
Self {
|
||||
path,
|
||||
mime,
|
||||
buf_chunk_size: DEFAULT_CAPACITY,
|
||||
precompressed_variants: None,
|
||||
}
|
||||
Self(ServeDir::new_single_file(path, mime))
|
||||
}
|
||||
|
||||
/// Create a new [`ServeFile`] with a specific mime type.
|
||||
@@ -63,14 +38,7 @@ impl ServeFile {
|
||||
/// [header value]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html
|
||||
pub fn new_with_mime<P: AsRef<Path>>(path: P, mime: &Mime) -> Self {
|
||||
let mime = HeaderValue::from_str(mime.as_ref()).expect("mime isn't a valid header value");
|
||||
let path = path.as_ref().to_owned();
|
||||
|
||||
Self {
|
||||
path,
|
||||
mime,
|
||||
buf_chunk_size: DEFAULT_CAPACITY,
|
||||
precompressed_variants: None,
|
||||
}
|
||||
Self(ServeDir::new_single_file(path, mime))
|
||||
}
|
||||
|
||||
/// Informs the service that it should also look for a precompressed gzip
|
||||
@@ -83,11 +51,8 @@ impl ServeFile {
|
||||
/// Both the precompressed version and the uncompressed version are expected
|
||||
/// to be present in the same directory. Different precompressed
|
||||
/// variants can be combined.
|
||||
pub fn precompressed_gzip(mut self) -> Self {
|
||||
self.precompressed_variants
|
||||
.get_or_insert(Default::default())
|
||||
.gzip = true;
|
||||
self
|
||||
pub fn precompressed_gzip(self) -> Self {
|
||||
Self(self.0.precompressed_gzip())
|
||||
}
|
||||
|
||||
/// Informs the service that it should also look for a precompressed brotli
|
||||
@@ -100,11 +65,8 @@ impl ServeFile {
|
||||
/// Both the precompressed version and the uncompressed version are expected
|
||||
/// to be present in the same directory. Different precompressed
|
||||
/// variants can be combined.
|
||||
pub fn precompressed_br(mut self) -> Self {
|
||||
self.precompressed_variants
|
||||
.get_or_insert(Default::default())
|
||||
.br = true;
|
||||
self
|
||||
pub fn precompressed_br(self) -> Self {
|
||||
Self(self.0.precompressed_br())
|
||||
}
|
||||
|
||||
/// Informs the service that it should also look for a precompressed deflate
|
||||
@@ -117,31 +79,22 @@ impl ServeFile {
|
||||
/// Both the precompressed version and the uncompressed version are expected
|
||||
/// to be present in the same directory. Different precompressed
|
||||
/// variants can be combined.
|
||||
pub fn precompressed_deflate(mut self) -> Self {
|
||||
self.precompressed_variants
|
||||
.get_or_insert(Default::default())
|
||||
.deflate = true;
|
||||
self
|
||||
pub fn precompressed_deflate(self) -> Self {
|
||||
Self(self.0.precompressed_deflate())
|
||||
}
|
||||
|
||||
/// Set a specific read buffer chunk size.
|
||||
///
|
||||
/// The default capacity is 64kb.
|
||||
pub fn with_buf_chunk_size(mut self, chunk_size: usize) -> Self {
|
||||
self.buf_chunk_size = chunk_size;
|
||||
self
|
||||
pub fn with_buf_chunk_size(self, chunk_size: usize) -> Self {
|
||||
Self(self.0.with_buf_chunk_size(chunk_size))
|
||||
}
|
||||
}
|
||||
|
||||
enum FileReadFuture {
|
||||
Open(FileFuture),
|
||||
Metadata(Pin<Box<dyn Future<Output = io::Result<(Metadata, Option<Encoding>)>>>>),
|
||||
}
|
||||
|
||||
impl<ReqBody> Service<Request<ReqBody>> for ServeFile {
|
||||
type Error = io::Error;
|
||||
type Response = Response<ResponseBody>;
|
||||
type Future = ResponseFuture;
|
||||
type Error = <ServeDir as Service<Request<ReqBody>>>::Error;
|
||||
type Response = <ServeDir as Service<Request<ReqBody>>>::Response;
|
||||
type Future = <ServeDir as Service<Request<ReqBody>>>::Future;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
@@ -149,112 +102,21 @@ impl<ReqBody> Service<Request<ReqBody>> for ServeFile {
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let path = self.path.clone();
|
||||
// The negotiated encodings based on the Accept-Encoding header and
|
||||
// precompressed variants
|
||||
let negotiated_encodings = encodings(
|
||||
req.headers(),
|
||||
self.precompressed_variants.unwrap_or_default(),
|
||||
);
|
||||
let file_future = match *req.method() {
|
||||
Method::HEAD => FileReadFuture::Metadata(Box::pin(file_metadata_with_fallback(
|
||||
path,
|
||||
negotiated_encodings,
|
||||
))),
|
||||
_ => FileReadFuture::Open(Box::pin(open_file_with_fallback(
|
||||
path,
|
||||
negotiated_encodings,
|
||||
))),
|
||||
};
|
||||
|
||||
ResponseFuture {
|
||||
file_future,
|
||||
mime: Some(self.mime.clone()),
|
||||
buf_chunk_size: self.buf_chunk_size,
|
||||
}
|
||||
self.0.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
/// Response future of [`ServeFile`].
|
||||
pub struct ResponseFuture {
|
||||
file_future: FileReadFuture,
|
||||
mime: Option<HeaderValue>,
|
||||
buf_chunk_size: usize,
|
||||
}
|
||||
|
||||
impl Future for ResponseFuture {
|
||||
type Output = io::Result<Response<ResponseBody>>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match &mut self.file_future {
|
||||
FileReadFuture::Open(open_file_future) => {
|
||||
let (file, maybe_encoding) = match open_file_future.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok((file, maybe_encoding))) => (file, maybe_encoding),
|
||||
Poll::Ready(Err(err)) => {
|
||||
return Poll::Ready(
|
||||
super::response_from_io_error(err)
|
||||
.map(|res| res.map(ResponseBody::new)),
|
||||
)
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
};
|
||||
let chunk_size = self.buf_chunk_size;
|
||||
let body = AsyncReadBody::with_capacity(file, chunk_size).boxed();
|
||||
let body = ResponseBody::new(body);
|
||||
let mut res = Response::new(body);
|
||||
|
||||
res.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, self.mime.take().unwrap());
|
||||
|
||||
if let Some(encoding) = maybe_encoding {
|
||||
res.headers_mut()
|
||||
.insert(header::CONTENT_ENCODING, encoding.into_header_value());
|
||||
}
|
||||
Poll::Ready(Ok(res))
|
||||
}
|
||||
FileReadFuture::Metadata(metadata_future) => {
|
||||
let (metadata, maybe_encoding) = match metadata_future.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok((metadata, maybe_encoding))) => (metadata, maybe_encoding),
|
||||
Poll::Ready(Err(err)) => {
|
||||
return Poll::Ready(
|
||||
super::response_from_io_error(err)
|
||||
.map(|res| res.map(ResponseBody::new)),
|
||||
)
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
};
|
||||
|
||||
let mut res = Response::new(ResponseBody::new(BoxBody::default()));
|
||||
|
||||
res.headers_mut()
|
||||
.insert(header::CONTENT_LENGTH, metadata.len().into());
|
||||
res.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, self.mime.take().unwrap());
|
||||
|
||||
if let Some(encoding) = maybe_encoding {
|
||||
res.headers_mut()
|
||||
.insert(header::CONTENT_ENCODING, encoding.into_header_value());
|
||||
}
|
||||
Poll::Ready(Ok(res))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opaque_body! {
|
||||
/// Response body for [`ServeFile`].
|
||||
pub type ResponseBody = BoxBody<Bytes, io::Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
use brotli::BrotliDecompress;
|
||||
use flate2::bufread::DeflateDecoder;
|
||||
use flate2::bufread::GzDecoder;
|
||||
use http::header;
|
||||
use http::Method;
|
||||
use http::{Request, StatusCode};
|
||||
use http_body::Body as _;
|
||||
@@ -275,6 +137,20 @@ mod tests {
|
||||
assert!(body.starts_with("# Tower HTTP"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_with_mime() {
|
||||
let svc = ServeFile::new_with_mime("../README.md", &Mime::from_str("image/jpg").unwrap());
|
||||
|
||||
let res = svc.oneshot(Request::new(Body::empty())).await.unwrap();
|
||||
|
||||
assert_eq!(res.headers()["content-type"], "image/jpg");
|
||||
|
||||
let body = res.into_body().data().await.unwrap().unwrap();
|
||||
let body = String::from_utf8(body.to_vec()).unwrap();
|
||||
|
||||
assert!(body.starts_with("# Tower HTTP"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn head_request() {
|
||||
let svc = ServeFile::new("../test-files/precompressed.txt");
|
||||
|
||||
Reference in New Issue
Block a user