mirror of https://github.com/fafhrd91/actix-web
feat(file): add support for extracting multi-component path params (#4039)
This commit is contained in:
parent
86eeea7f92
commit
1aa74f4234
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add support for passing multiple root directories to `Files::new`. [#3402]
|
||||||
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
|
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
|
||||||
- Fix handling of `bytes=0-`
|
- Fix handling of `bytes=0-`
|
||||||
- Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748]
|
- Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748]
|
||||||
- Fix invalid `Content-Encoding: identity` header in `NamedFile` range responses. [#3191]
|
- Fix invalid `Content-Encoding: identity` header in `NamedFile` range responses. [#3191]
|
||||||
|
|
||||||
|
[#3402]: https://github.com/actix/actix-web/issues/3402
|
||||||
[#2615]: https://github.com/actix/actix-web/pull/2615
|
[#2615]: https://github.com/actix/actix-web/pull/2615
|
||||||
[#2748]: https://github.com/actix/actix-web/issues/2748
|
[#2748]: https://github.com/actix/actix-web/issues/2748
|
||||||
[#3191]: https://github.com/actix/actix-web/issues/3191
|
[#3191]: https://github.com/actix/actix-web/issues/3191
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
ffi::{OsStr, OsString},
|
||||||
fmt, io,
|
fmt, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
|
@ -37,7 +39,7 @@ use crate::{
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
mount_path: String,
|
mount_path: String,
|
||||||
directory: PathBuf,
|
directories: Vec<PathBuf>,
|
||||||
index: Option<String>,
|
index: Option<String>,
|
||||||
show_index: bool,
|
show_index: bool,
|
||||||
redirect_to_slash: bool,
|
redirect_to_slash: bool,
|
||||||
|
|
@ -63,7 +65,7 @@ impl fmt::Debug for Files {
|
||||||
impl Clone for Files {
|
impl Clone for Files {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
directory: self.directory.clone(),
|
directories: self.directories.clone(),
|
||||||
index: self.index.clone(),
|
index: self.index.clone(),
|
||||||
show_index: self.show_index,
|
show_index: self.show_index,
|
||||||
redirect_to_slash: self.redirect_to_slash,
|
redirect_to_slash: self.redirect_to_slash,
|
||||||
|
|
@ -83,6 +85,131 @@ impl Clone for Files {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// File serving root directories for [`Files`].
|
||||||
|
///
|
||||||
|
/// This type is used by [`Files::new`] to accept either one root directory or an ordered
|
||||||
|
/// collection of root directories.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FilesDirs(Vec<PathBuf>);
|
||||||
|
|
||||||
|
impl FilesDirs {
|
||||||
|
fn canonicalize(self) -> Vec<PathBuf> {
|
||||||
|
self.0
|
||||||
|
.into_iter()
|
||||||
|
.map(|orig_dir| match orig_dir.canonicalize() {
|
||||||
|
Ok(canon_dir) => canon_dir,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||||
|
// Preserve original path so requests don't fall back to CWD.
|
||||||
|
orig_dir
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Path> for FilesDirs {
|
||||||
|
fn from(dir: &Path) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PathBuf> for FilesDirs {
|
||||||
|
fn from(dir: &PathBuf) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for FilesDirs {
|
||||||
|
fn from(dir: PathBuf) -> Self {
|
||||||
|
Self(vec![dir])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for FilesDirs {
|
||||||
|
fn from(dir: &str) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&String> for FilesDirs {
|
||||||
|
fn from(dir: &String) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for FilesDirs {
|
||||||
|
fn from(dir: String) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&OsStr> for FilesDirs {
|
||||||
|
fn from(dir: &OsStr) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OsString> for FilesDirs {
|
||||||
|
fn from(dir: OsString) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&OsString> for FilesDirs {
|
||||||
|
fn from(dir: &OsString) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Box<Path>> for FilesDirs {
|
||||||
|
fn from(dir: Box<Path>) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Cow<'_, Path>> for FilesDirs {
|
||||||
|
fn from(dir: Cow<'_, Path>) -> Self {
|
||||||
|
Self(vec![dir.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, const N: usize> From<[P; N]> for FilesDirs
|
||||||
|
where
|
||||||
|
P: Into<PathBuf>,
|
||||||
|
{
|
||||||
|
fn from(dirs: [P; N]) -> Self {
|
||||||
|
Self(dirs.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, const N: usize> From<&[P; N]> for FilesDirs
|
||||||
|
where
|
||||||
|
P: Clone + Into<PathBuf>,
|
||||||
|
{
|
||||||
|
fn from(dirs: &[P; N]) -> Self {
|
||||||
|
Self(dirs.iter().cloned().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<&[P]> for FilesDirs
|
||||||
|
where
|
||||||
|
P: Clone + Into<PathBuf>,
|
||||||
|
{
|
||||||
|
fn from(dirs: &[P]) -> Self {
|
||||||
|
Self(dirs.iter().cloned().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<Vec<P>> for FilesDirs
|
||||||
|
where
|
||||||
|
P: Into<PathBuf>,
|
||||||
|
{
|
||||||
|
fn from(dirs: Vec<P>) -> Self {
|
||||||
|
Self(dirs.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Files {
|
impl Files {
|
||||||
/// Create new `Files` instance for a specified base directory.
|
/// Create new `Files` instance for a specified base directory.
|
||||||
///
|
///
|
||||||
|
|
@ -90,34 +217,34 @@ impl Files {
|
||||||
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
/// The first argument (`mount_path`) is the root URL at which the static files are served.
|
||||||
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
/// For example, `/assets` will serve files at `example.com/assets/...`.
|
||||||
///
|
///
|
||||||
/// The second argument (`serve_from`) is the location on disk at which files are loaded.
|
/// The second argument (`serve_from`) is the location on disk that files are served from. This
|
||||||
/// This can be a relative path. For example, `./` would serve files from the current
|
/// can be a single path or an ordered collection of paths. Relative paths are resolved from the
|
||||||
/// working directory.
|
/// current working directory.
|
||||||
|
///
|
||||||
|
/// When multiple directories are provided, they are checked in order. The first directory that
|
||||||
|
/// can serve the requested path is used.
|
||||||
|
///
|
||||||
|
/// Directory listings are generated from the first matching directory and are not merged across
|
||||||
|
/// roots. When [`Files::index_file()`] is configured, later roots are searched if an earlier
|
||||||
|
/// matching directory does not contain the index file.
|
||||||
|
///
|
||||||
|
/// Empty root collections never match files; requests fall through to the default handler, or
|
||||||
|
/// return `404 Not Found` if none is configured.
|
||||||
///
|
///
|
||||||
/// # Implementation Notes
|
/// # Implementation Notes
|
||||||
/// If the mount path is set as the root path `/`, services registered after this one will
|
/// If the mount path is set as the root path `/`, services registered after this one will
|
||||||
/// be inaccessible. Register more specific handlers and services first.
|
/// be inaccessible. Register more specific handlers and services first.
|
||||||
///
|
///
|
||||||
/// If `serve_from` cannot be canonicalized at startup, an error is logged and the original
|
/// If a `serve_from` path cannot be canonicalized at startup, an error is logged and the
|
||||||
/// path is preserved. Requests will return `404 Not Found` until the path exists.
|
/// original path is preserved. Requests will return `404 Not Found` until the path exists.
|
||||||
///
|
///
|
||||||
/// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
|
/// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
|
||||||
/// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
|
/// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
|
||||||
/// the number of server [workers](actix_web::HttpServer::workers), by default.
|
/// the number of server [workers](actix_web::HttpServer::workers), by default.
|
||||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
pub fn new<T: Into<FilesDirs>>(mount_path: &str, serve_from: T) -> Files {
|
||||||
let orig_dir = serve_from.into();
|
|
||||||
let dir = match orig_dir.canonicalize() {
|
|
||||||
Ok(canon_dir) => canon_dir,
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
|
||||||
// Preserve original path so requests don't fall back to CWD.
|
|
||||||
orig_dir
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Files {
|
Files {
|
||||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||||
directory: dir,
|
directories: serve_from.into().canonicalize(),
|
||||||
index: None,
|
index: None,
|
||||||
show_index: false,
|
show_index: false,
|
||||||
redirect_to_slash: false,
|
redirect_to_slash: false,
|
||||||
|
|
@ -149,6 +276,9 @@ impl Files {
|
||||||
/// Redirects to a slash-ended path when browsing a directory.
|
/// Redirects to a slash-ended path when browsing a directory.
|
||||||
///
|
///
|
||||||
/// By default never redirect.
|
/// By default never redirect.
|
||||||
|
///
|
||||||
|
/// When multiple root directories are configured, a matching directory in an earlier root can
|
||||||
|
/// trigger a redirect before later roots are checked for a file at the same path.
|
||||||
pub fn redirect_to_slash_directory(mut self) -> Self {
|
pub fn redirect_to_slash_directory(mut self) -> Self {
|
||||||
self.redirect_to_slash = true;
|
self.redirect_to_slash = true;
|
||||||
self
|
self
|
||||||
|
|
@ -407,7 +537,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
let mut inner = FilesServiceInner {
|
let mut inner = FilesServiceInner {
|
||||||
directory: self.directory.clone(),
|
directories: self.directories.clone(),
|
||||||
index: self.index.clone(),
|
index: self.index.clone(),
|
||||||
show_index: self.show_index,
|
show_index: self.show_index,
|
||||||
redirect_to_slash: self.redirect_to_slash,
|
redirect_to_slash: self.redirect_to_slash,
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,14 @@ mod range;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
chunked::ChunkedReadFile, directory::Directory, error::UriSegmentError, files::Files,
|
chunked::ChunkedReadFile,
|
||||||
named::NamedFile, path_buf::PathBufWrap, range::HttpRange, service::FilesService,
|
directory::Directory,
|
||||||
|
error::UriSegmentError,
|
||||||
|
files::{Files, FilesDirs},
|
||||||
|
named::NamedFile,
|
||||||
|
path_buf::PathBufWrap,
|
||||||
|
range::HttpRange,
|
||||||
|
service::FilesService,
|
||||||
};
|
};
|
||||||
use self::{
|
use self::{
|
||||||
directory::{directory_listing, DirectoryRenderer},
|
directory::{directory_listing, DirectoryRenderer},
|
||||||
|
|
@ -63,9 +69,11 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
|
ffi::OsString,
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
fs::{self},
|
fs::{self},
|
||||||
ops::Add,
|
ops::Add,
|
||||||
|
path::PathBuf,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -832,6 +840,243 @@ mod tests {
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_accepts_borrowed_os_string_directory() {
|
||||||
|
let dir = OsString::from(".");
|
||||||
|
let service = Files::new("/", &dir).new_service(()).await.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_empty_directories() {
|
||||||
|
let service = Files::new("/", Vec::<PathBuf>::new())
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let service = Files::new("/", Vec::<PathBuf>::new())
|
||||||
|
.default_handler(|req: ServiceRequest| async {
|
||||||
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
|
})
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
test::read_body(resp).await,
|
||||||
|
Bytes::from_static(b"default content")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::write(first_dir.path().join("shared.txt"), "first").unwrap();
|
||||||
|
fs::write(second_dir.path().join("shared.txt"), "second").unwrap();
|
||||||
|
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/shared.txt").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"first"));
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_file_as_parent_falls_back() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::write(first_dir.path().join("assets"), "file").unwrap();
|
||||||
|
fs::create_dir(second_dir.path().join("assets")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
second_dir.path().join("assets").join("fallback.txt"),
|
||||||
|
"fallback",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/assets/fallback.txt").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_default_handler() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::write(second_dir.path().join("fallback.txt"), "fallback").unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", vec![first_dir.path(), second_dir.path()])
|
||||||
|
.default_handler(|req: ServiceRequest| async {
|
||||||
|
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||||
|
})
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/fallback.txt").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/missing.txt").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
test::read_body(resp).await,
|
||||||
|
Bytes::from_static(b"default content")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_index_file() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::create_dir(first_dir.path().join("nested")).unwrap();
|
||||||
|
fs::create_dir(second_dir.path().join("nested")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
second_dir.path().join("nested").join("index.html"),
|
||||||
|
"second index",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.index_file("index.html")
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/nested/").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
test::read_body(resp).await,
|
||||||
|
Bytes::from_static(b"second index")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_index_file_as_parent_falls_back() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::create_dir(first_dir.path().join("nested")).unwrap();
|
||||||
|
fs::write(first_dir.path().join("nested").join("index.html"), "file").unwrap();
|
||||||
|
fs::create_dir(second_dir.path().join("nested")).unwrap();
|
||||||
|
fs::create_dir(second_dir.path().join("nested").join("index.html")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
second_dir
|
||||||
|
.path()
|
||||||
|
.join("nested")
|
||||||
|
.join("index.html")
|
||||||
|
.join("fallback.txt"),
|
||||||
|
"fallback",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.index_file("index.html/fallback.txt")
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/nested/").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
assert_eq!(test::read_body(resp).await, Bytes::from_static(b"fallback"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_index_file_error_falls_back_to_listing() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::write(dir.path().join("listed.txt"), "listed").unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", dir.path())
|
||||||
|
.index_file("index.html\0")
|
||||||
|
.show_files_listing()
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let bytes = test::read_body(resp).await;
|
||||||
|
assert!(format!("{bytes:?}").contains("listed.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_show_files_listing() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::write(first_dir.path().join("listed.txt"), "listed").unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.show_files_listing()
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
let bytes = test::read_body(resp).await;
|
||||||
|
assert!(format!("{bytes:?}").contains("listed.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_static_files_multiple_directories_redirect_precedence() {
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
fs::create_dir(first_dir.path().join("item")).unwrap();
|
||||||
|
fs::write(second_dir.path().join("item"), "file").unwrap();
|
||||||
|
|
||||||
|
let service = Files::new("/", [first_dir.path(), second_dir.path()])
|
||||||
|
.show_files_listing()
|
||||||
|
.redirect_to_slash_directory()
|
||||||
|
.new_service(())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/item").to_srv_request();
|
||||||
|
let resp = test::call_service(&service, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT);
|
||||||
|
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/item/");
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_default_handler_file_missing() {
|
async fn test_default_handler_file_missing() {
|
||||||
let st = Files::new("/", ".")
|
let st = Files::new("/", ".")
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ impl Deref for FilesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FilesServiceInner {
|
pub struct FilesServiceInner {
|
||||||
pub(crate) directory: PathBuf,
|
pub(crate) directories: Vec<PathBuf>,
|
||||||
pub(crate) index: Option<String>,
|
pub(crate) index: Option<String>,
|
||||||
pub(crate) show_index: bool,
|
pub(crate) show_index: bool,
|
||||||
pub(crate) redirect_to_slash: bool,
|
pub(crate) redirect_to_slash: bool,
|
||||||
|
|
@ -113,8 +113,8 @@ impl FilesService {
|
||||||
self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity)
|
self.serve_named_file_with_encoding(req, named_file, header::ContentEncoding::Identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse {
|
||||||
let dir = Directory::new(self.directory.clone(), path);
|
let dir = Directory::new(base, path);
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
|
|
||||||
|
|
@ -171,8 +171,13 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut last_miss = None;
|
||||||
|
let mut first_index_listing = None;
|
||||||
|
let mut found_unrenderable_dir = false;
|
||||||
|
|
||||||
|
for directory in &this.directories {
|
||||||
// full file path
|
// full file path
|
||||||
let path = this.directory.join(&path_on_disk);
|
let path = directory.join(&path_on_disk);
|
||||||
|
|
||||||
// Try serving pre-compressed file even if the uncompressed file doesn't exist yet.
|
// Try serving pre-compressed file even if the uncompressed file doesn't exist yet.
|
||||||
// Still handle directories (index/listing) through the normal branch below.
|
// Still handle directories (index/listing) through the normal branch below.
|
||||||
|
|
@ -183,6 +188,14 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = path.canonicalize() {
|
if let Err(err) = path.canonicalize() {
|
||||||
|
if matches!(
|
||||||
|
err.kind(),
|
||||||
|
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||||
|
) {
|
||||||
|
last_miss = Some(err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return this.handle_err(err, req).await;
|
return this.handle_err(err, req).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,37 +217,78 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
return Ok(req.into_response(response));
|
return Ok(req.into_response(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
match this.index {
|
match &this.index {
|
||||||
Some(ref index) => {
|
Some(index) => {
|
||||||
let named_path = path.join(index);
|
let named_path = path.join(index);
|
||||||
if this.try_compressed {
|
if this.try_compressed {
|
||||||
if let Some((named_file, encoding)) =
|
if let Some((named_file, encoding)) =
|
||||||
find_compressed(&req, &named_path).await
|
find_compressed(&req, &named_path).await
|
||||||
{
|
{
|
||||||
return Ok(
|
return Ok(this.serve_named_file_with_encoding(
|
||||||
this.serve_named_file_with_encoding(req, named_file, encoding)
|
req, named_file, encoding,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fallback to the uncompressed version
|
// fallback to the uncompressed version
|
||||||
match NamedFile::open_async(named_path).await {
|
match NamedFile::open_async(named_path).await {
|
||||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
|
||||||
Err(_) if this.show_index => Ok(this.show_index(req, path)),
|
Err(err)
|
||||||
Err(err) => this.handle_err(err, req).await,
|
if matches!(
|
||||||
|
err.kind(),
|
||||||
|
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
if this.show_index && first_index_listing.is_none() {
|
||||||
|
first_index_listing =
|
||||||
|
Some((directory.to_path_buf(), path.clone()));
|
||||||
|
}
|
||||||
|
last_miss = Some(err);
|
||||||
|
}
|
||||||
|
Err(_) if this.show_index => {
|
||||||
|
if first_index_listing.is_none() {
|
||||||
|
first_index_listing =
|
||||||
|
Some((directory.to_path_buf(), path.clone()));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => return this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None if this.show_index => Ok(this.show_index(req, path)),
|
None if this.show_index => {
|
||||||
None => Ok(ServiceResponse::from_err(
|
return Ok(this.show_index(req, directory.to_path_buf(), path));
|
||||||
FilesError::IsDirectory,
|
}
|
||||||
req.into_parts().0,
|
None => found_unrenderable_dir = true,
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match NamedFile::open_async(&path).await {
|
match NamedFile::open_async(&path).await {
|
||||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
Ok(named_file) => return Ok(this.serve_named_file(req, named_file)),
|
||||||
Err(err) => this.handle_err(err, req).await,
|
Err(err)
|
||||||
|
if matches!(
|
||||||
|
err.kind(),
|
||||||
|
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
last_miss = Some(err);
|
||||||
|
}
|
||||||
|
Err(err) => return this.handle_err(err, req).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((base, path)) = first_index_listing {
|
||||||
|
return Ok(this.show_index(req, base, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_unrenderable_dir {
|
||||||
|
return Ok(ServiceResponse::from_err(
|
||||||
|
FilesError::IsDirectory,
|
||||||
|
req.into_parts().0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = last_miss
|
||||||
|
.unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No such file"));
|
||||||
|
this.handle_err(err, req).await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,44 @@ async fn test_compression_encodings() {
|
||||||
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
|
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_compression_encodings_multiple_directories() {
|
||||||
|
use actix_web::body::MessageBody;
|
||||||
|
|
||||||
|
let first_dir = tempfile::tempdir().unwrap();
|
||||||
|
let second_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let compressed_path = second_dir.path().join("fallback.txt.gz");
|
||||||
|
std::fs::write(&compressed_path, b"compressed").unwrap();
|
||||||
|
let compressed_len = std::fs::metadata(compressed_path).unwrap().len();
|
||||||
|
|
||||||
|
let srv = test::init_service(
|
||||||
|
App::new().service(Files::new("/", [first_dir.path(), second_dir.path()]).try_compressed()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut req = TestRequest::with_uri("/fallback.txt").to_request();
|
||||||
|
req.headers_mut().insert(
|
||||||
|
header::ACCEPT_ENCODING,
|
||||||
|
header::HeaderValue::from_static("gzip"),
|
||||||
|
);
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
res.headers().get(header::CONTENT_TYPE),
|
||||||
|
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res.headers().get(header::CONTENT_ENCODING),
|
||||||
|
Some(&HeaderValue::from_static("gzip")),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res.into_body().size(),
|
||||||
|
actix_web::body::BodySize::Sized(compressed_len),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn partial_range_response_encoding() {
|
async fn partial_range_response_encoding() {
|
||||||
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue