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]
|
||||
- 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
|
||||
[#3402]: https://github.com/actix/actix-web/issues/3402
|
||||
|
||||
## 0.6.10
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,19 @@ use crate::{
|
|||
/// let app = App::new()
|
||||
/// .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 {
|
||||
mount_path: String,
|
||||
directory: PathBuf,
|
||||
directories: Vec<PathBuf>,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
redirect_to_slash: bool,
|
||||
|
|
@ -63,7 +73,7 @@ impl fmt::Debug for Files {
|
|||
impl Clone for Files {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
directory: self.directory.clone(),
|
||||
directories: self.directories.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
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 server [workers](actix_web::HttpServer::workers), by default.
|
||||
pub fn new<T: Into<PathBuf>>(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
|
||||
}
|
||||
};
|
||||
Self::new_multiple(mount_path, std::iter::once(serve_from))
|
||||
}
|
||||
|
||||
/// Create new `Files` instance for specified base directories from an array.
|
||||
///
|
||||
/// # 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 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 {
|
||||
mount_path: mount_path.trim_end_matches('/').to_owned(),
|
||||
directory: dir,
|
||||
directories,
|
||||
index: None,
|
||||
show_index: false,
|
||||
redirect_to_slash: false,
|
||||
|
|
@ -403,7 +445,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut inner = FilesServiceInner {
|
||||
directory: self.directory.clone(),
|
||||
directories: self.directories.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
redirect_to_slash: self.redirect_to_slash,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl Deref for FilesService {
|
|||
}
|
||||
|
||||
pub struct FilesServiceInner {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) directories: Vec<PathBuf>,
|
||||
pub(crate) index: Option<String>,
|
||||
pub(crate) show_index: 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)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
let dir = Directory::new(path.clone(), path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
|
||||
|
|
@ -171,8 +174,34 @@ impl Service<ServiceRequest> for FilesService {
|
|||
}
|
||||
}
|
||||
|
||||
// full file path
|
||||
let path = this.directory.join(&path_on_disk);
|
||||
// Try to find file in multiple directories
|
||||
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.
|
||||
// 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 this.redirect_to_slash
|
||||
&& !req.path().ends_with('/')
|
||||
|
|
|
|||
|
|
@ -193,3 +193,72 @@ async fn partial_range_response_encoding() {
|
|||
"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