mirror of https://github.com/fafhrd91/actix-web
Add multi-directory static file serving
This commit is contained in:
parent
d66f89b7b6
commit
02e4bef723
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
- 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-`
|
||||||
|
- Add `Files::new_from_array()` and `Files::new_multiple()` methods to support multiple directories from array and iterator, and implement multi-directory static file serving with priority order [#3402]
|
||||||
|
|
||||||
[#2615]: https://github.com/actix/actix-web/pull/2615
|
[#2615]: https://github.com/actix/actix-web/pull/2615
|
||||||
|
[#3402]: https://github.com/actix/actix-web/issues/3402
|
||||||
|
|
||||||
## 0.6.10
|
## 0.6.10
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,19 @@ use crate::{
|
||||||
/// let app = App::new()
|
/// let app = App::new()
|
||||||
/// .service(Files::new("/static", "."));
|
/// .service(Files::new("/static", "."));
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Multiple Directories Example
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::App;
|
||||||
|
/// use actix_files::Files;
|
||||||
|
///
|
||||||
|
/// // Serve files from multiple directories with priority order
|
||||||
|
/// let app = App::new()
|
||||||
|
/// .service(Files::new_from_array("/static", &["./dist", "./public"]));
|
||||||
|
/// ```
|
||||||
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 +73,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,
|
||||||
|
|
@ -105,19 +115,51 @@ impl Files {
|
||||||
/// 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<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||||
let orig_dir = serve_from.into();
|
Self::new_multiple(mount_path, std::iter::once(serve_from))
|
||||||
let dir = match orig_dir.canonicalize() {
|
}
|
||||||
Ok(canon_dir) => canon_dir,
|
|
||||||
Err(_) => {
|
/// Create new `Files` instance for specified base directories from an array.
|
||||||
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
///
|
||||||
// Preserve original path so requests don't fall back to CWD.
|
/// # Argument Order
|
||||||
orig_dir
|
/// 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/...`.
|
||||||
};
|
///
|
||||||
|
/// The second argument (`serve_from`) is an array of paths on disk at which files are loaded.
|
||||||
|
/// For example, `["./dist", "./public"]` would serve files from both directories in order of priority.
|
||||||
|
pub fn new_from_array<T: Into<PathBuf> + Clone>(mount_path: &str, serve_from: &[T]) -> Files {
|
||||||
|
Self::new_multiple(mount_path, serve_from.iter().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new `Files` instance for specified base directories from an iterator.
|
||||||
|
///
|
||||||
|
/// # Argument Order
|
||||||
|
/// 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/...`.
|
||||||
|
///
|
||||||
|
/// The second argument (`serve_from`) is an iterator of paths on disk at which files are loaded.
|
||||||
|
/// For example, `vec!["./dist", "./public"].into_iter()` would serve files from both directories in order of priority.
|
||||||
|
pub fn new_multiple<I, T>(mount_path: &str, serve_from: I) -> Files
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = T>,
|
||||||
|
T: Into<PathBuf>,
|
||||||
|
{
|
||||||
|
let directories = serve_from
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let orig_dir = path.into();
|
||||||
|
match orig_dir.canonicalize() {
|
||||||
|
Ok(canon_dir) => canon_dir,
|
||||||
|
Err(_) => {
|
||||||
|
log::error!("Specified path is not a directory: {:?}", orig_dir);
|
||||||
|
orig_dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Files {
|
Files {
|
||||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||||
directory: dir,
|
directories,
|
||||||
index: None,
|
index: None,
|
||||||
show_index: false,
|
show_index: false,
|
||||||
redirect_to_slash: false,
|
redirect_to_slash: false,
|
||||||
|
|
@ -403,7 +445,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,
|
||||||
|
|
|
||||||
|
|
@ -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,11 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show index listing for a directory.
|
||||||
|
///
|
||||||
|
/// Uses the directory where the path was found as the base directory for index listing.
|
||||||
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||||
let dir = Directory::new(self.directory.clone(), path);
|
let dir = Directory::new(path.clone(), path);
|
||||||
|
|
||||||
let (req, _) = req.into_parts();
|
let (req, _) = req.into_parts();
|
||||||
|
|
||||||
|
|
@ -171,8 +174,34 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// full file path
|
// Try to find file in multiple directories
|
||||||
let path = this.directory.join(&path_on_disk);
|
let mut found_path = None;
|
||||||
|
let mut last_err = None;
|
||||||
|
|
||||||
|
for directory in &this.directories {
|
||||||
|
let path = directory.join(&path_on_disk);
|
||||||
|
match path.canonicalize() {
|
||||||
|
Ok(canonical_path) => {
|
||||||
|
found_path = Some(canonical_path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Keep track of the last error
|
||||||
|
last_err = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = match found_path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
// If all directories failed, use the last error
|
||||||
|
let err = last_err.unwrap_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::NotFound, "File not found")
|
||||||
|
});
|
||||||
|
return this.handle_err(err, req).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
@ -182,10 +211,6 @@ impl Service<ServiceRequest> for FilesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = path.canonicalize() {
|
|
||||||
return this.handle_err(err, req).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
if this.redirect_to_slash
|
if this.redirect_to_slash
|
||||||
&& !req.path().ends_with('/')
|
&& !req.path().ends_with('/')
|
||||||
|
|
|
||||||
|
|
@ -193,3 +193,72 @@ async fn partial_range_response_encoding() {
|
||||||
"identity"
|
"identity"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_multiple_directories() {
|
||||||
|
// Create test directories
|
||||||
|
std::fs::create_dir_all("./tests/test1").unwrap();
|
||||||
|
std::fs::create_dir_all("./tests/test2").unwrap();
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
std::fs::write("./tests/test1/test.txt", "File from test1").unwrap();
|
||||||
|
std::fs::write("./tests/test2/fallback.txt", "File from test2").unwrap();
|
||||||
|
|
||||||
|
// Test multiple directories with new_from_array
|
||||||
|
let srv = test::init_service(App::new().service(Files::new_from_array(
|
||||||
|
"/",
|
||||||
|
&["./tests/test1", "./tests/test2"],
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Test file from first directory
|
||||||
|
let req = TestRequest::with_uri("/test.txt").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
let body = test::read_body(res).await;
|
||||||
|
assert_eq!(&body[..], b"File from test1");
|
||||||
|
|
||||||
|
// Test file from second directory
|
||||||
|
let req = TestRequest::with_uri("/fallback.txt").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
let body = test::read_body(res).await;
|
||||||
|
assert_eq!(&body[..], b"File from test2");
|
||||||
|
|
||||||
|
// Test non-existent file
|
||||||
|
let req = TestRequest::with_uri("/non-existent.txt").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = std::fs::remove_dir_all("./tests/test1");
|
||||||
|
let _ = std::fs::remove_dir_all("./tests/test2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_multiple_directories_iterator() {
|
||||||
|
// Create test directories
|
||||||
|
std::fs::create_dir_all("./tests/test1").unwrap();
|
||||||
|
std::fs::create_dir_all("./tests/test2").unwrap();
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
std::fs::write("./tests/test1/test.txt", "File from test1").unwrap();
|
||||||
|
|
||||||
|
// Test multiple directories with new_multiple
|
||||||
|
let srv = test::init_service(App::new().service(Files::new_multiple(
|
||||||
|
"/",
|
||||||
|
vec!["./tests/test1", "./tests/test2"],
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Test file from first directory
|
||||||
|
let req = TestRequest::with_uri("/test.txt").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
let body = test::read_body(res).await;
|
||||||
|
assert_eq!(&body[..], b"File from test1");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = std::fs::remove_dir_all("./tests/test1");
|
||||||
|
let _ = std::fs::remove_dir_all("./tests/test2");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue