Static Files refactoring (#604)

* Return index file instead of redirect

* Migrate configuration options to StaticFilesConfig
This commit is contained in:
Douman 2018-11-28 19:15:18 +03:00
parent ac9fc662c6
commit 580dcb8d6f
3 changed files with 204 additions and 163 deletions

View File

@ -1,5 +1,13 @@
# Changes # Changes
## [0.8.0] - 2019-xx-xx
## Changed
* `StaticFiles` no longer redirects to index file and instead attempts to return its content
* `StaticFiles` configuration is moved to `StaticFileConfig` completely.
## [0.7.15] - 2018-12-05 ## [0.7.15] - 2018-12-05
## Changed ## Changed

View File

@ -1,3 +1,12 @@
## 0.8.0
`StaticFiles` configuration methods are removed:
- `show_files_listing` - use custom configuration with `StaticFileConfig::show_index`
- `files_listing_renderer` - use custom configuration with `StaticFileConfig::directory_listing`
- `index_file` - use custom configuration with `StaticFileConfig::index_file`
- `default_handler` - use custom configuration with `StaticFileConfig::default_handler`
## 0.7.15 ## 0.7.15
* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in * The `' '` character is not percent decoded anymore before matching routes. If you need to use it in

320
src/fs.rs
View File

@ -20,7 +20,7 @@ use mime_guess::{get_mime_type, guess_mime_type};
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use error::{Error, StaticFileError}; use error::{Error, StaticFileError};
use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler}; use handler::{AsyncResult, Handler, Responder};
use header; use header;
use header::{ContentDisposition, DispositionParam, DispositionType}; use header::{ContentDisposition, DispositionParam, DispositionType};
use http::{ContentEncoding, Method, StatusCode}; use http::{ContentEncoding, Method, StatusCode};
@ -88,6 +88,37 @@ pub trait StaticFileConfig: Default {
fn is_method_allowed(_method: &Method) -> bool { fn is_method_allowed(_method: &Method) -> bool {
true true
} }
/// Set index file
///
/// Redirects to specific index file for directory "/" instead of
/// showing files listing.
///
/// By default None
fn index_file() -> Option<&'static str> {
None
}
/// Show files listing for directories.
///
/// By default show files listing is disabled.
fn show_index() -> bool {
false
}
/// Directory renderer
///
/// Uses default one, unless re-defined.
fn directory_listing<S>(dir: &Directory, req: &HttpRequest<S>) -> Result<HttpResponse, io::Error> {
directory_listing(dir, req)
}
/// Default handler for StaticFiles.
///
/// Responses with NotFound by default
fn default_handler<S>(_req: &HttpRequest<S>) -> AsyncResult<HttpResponse> {
HttpResponse::new(StatusCode::NOT_FOUND).into()
}
} }
///Default content disposition as described in ///Default content disposition as described in
@ -527,9 +558,6 @@ impl Stream for ChunkedReadFile {
} }
} }
type DirectoryRenderer<S> =
Fn(&Directory, &HttpRequest<S>) -> Result<HttpResponse, io::Error>;
/// A directory; responds with the generated directory listing. /// A directory; responds with the generated directory listing.
#[derive(Debug)] #[derive(Debug)]
pub struct Directory { pub struct Directory {
@ -644,25 +672,21 @@ fn directory_listing<S>(
/// .finish(); /// .finish();
/// } /// }
/// ``` /// ```
pub struct StaticFiles<S, C = DefaultConfig> { pub struct StaticFiles<C = DefaultConfig> {
directory: PathBuf, directory: PathBuf,
index: Option<String>,
show_index: bool,
cpu_pool: CpuPool, cpu_pool: CpuPool,
default: Box<RouteHandler<S>>,
renderer: Box<DirectoryRenderer<S>>,
_chunk_size: usize, _chunk_size: usize,
_follow_symlinks: bool, _follow_symlinks: bool,
_cd_map: PhantomData<C>, _cd_map: PhantomData<C>,
} }
impl<S: 'static> StaticFiles<S> { impl StaticFiles {
/// Create new `StaticFiles` instance for specified base directory. /// Create new `StaticFiles` instance for specified base directory.
/// ///
/// `StaticFile` uses `CpuPool` for blocking filesystem operations. /// `StaticFile` uses `CpuPool` for blocking filesystem operations.
/// By default pool with 20 threads is used. /// By default pool with 20 threads is used.
/// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable.
pub fn new<T: Into<PathBuf>>(dir: T) -> Result<StaticFiles<S>, Error> { pub fn new<T: Into<PathBuf>>(dir: T) -> Result<StaticFiles, Error> {
Self::with_config(dir, DefaultConfig) Self::with_config(dir, DefaultConfig)
} }
@ -671,19 +695,19 @@ impl<S: 'static> StaticFiles<S> {
pub fn with_pool<T: Into<PathBuf>>( pub fn with_pool<T: Into<PathBuf>>(
dir: T, dir: T,
pool: CpuPool, pool: CpuPool,
) -> Result<StaticFiles<S>, Error> { ) -> Result<StaticFiles, Error> {
Self::with_config_pool(dir, pool, DefaultConfig) Self::with_config_pool(dir, pool, DefaultConfig)
} }
} }
impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> { impl<C: StaticFileConfig> StaticFiles<C> {
/// Create new `StaticFiles` instance for specified base directory. /// Create new `StaticFiles` instance for specified base directory.
/// ///
/// Identical with `new` but allows to specify configiration to use. /// Identical with `new` but allows to specify configiration to use.
pub fn with_config<T: Into<PathBuf>>( pub fn with_config<T: Into<PathBuf>>(
dir: T, dir: T,
config: C, config: C,
) -> Result<StaticFiles<S, C>, Error> { ) -> Result<StaticFiles<C>, Error> {
// use default CpuPool // use default CpuPool
let pool = { DEFAULT_CPUPOOL.lock().clone() }; let pool = { DEFAULT_CPUPOOL.lock().clone() };
@ -696,7 +720,7 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
dir: T, dir: T,
pool: CpuPool, pool: CpuPool,
_: C, _: C,
) -> Result<StaticFiles<S, C>, Error> { ) -> Result<StaticFiles<C>, Error> {
let dir = dir.into().canonicalize()?; let dir = dir.into().canonicalize()?;
if !dir.is_dir() { if !dir.is_dir() {
@ -705,54 +729,14 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
Ok(StaticFiles { Ok(StaticFiles {
directory: dir, directory: dir,
index: None,
show_index: false,
cpu_pool: pool, cpu_pool: pool,
default: Box::new(WrapHandler::new(|_: &_| {
HttpResponse::new(StatusCode::NOT_FOUND)
})),
renderer: Box::new(directory_listing),
_chunk_size: 0, _chunk_size: 0,
_follow_symlinks: false, _follow_symlinks: false,
_cd_map: PhantomData, _cd_map: PhantomData,
}) })
} }
/// Show files listing for directories. fn try_handle<S: 'static>(
///
/// By default show files listing is disabled.
pub fn show_files_listing(mut self) -> Self {
self.show_index = true;
self
}
/// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest<S>)
-> Result<HttpResponse, io::Error>
+ 'static,
{
self.renderer = Box::new(f);
self
}
/// Set index file
///
/// Redirects to specific index file for directory "/" instead of
/// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> {
self.index = Some(index.into());
self
}
/// Sets default handler which is used when no matched file could be found.
pub fn default_handler<H: Handler<S>>(mut self, handler: H) -> StaticFiles<S, C> {
self.default = Box::new(WrapHandler::new(handler));
self
}
fn try_handle(
&self, &self,
req: &HttpRequest<S>, req: &HttpRequest<S>,
) -> Result<AsyncResult<HttpResponse>, Error> { ) -> Result<AsyncResult<HttpResponse>, Error> {
@ -760,44 +744,33 @@ impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?;
// full filepath // full filepath
let path = self.directory.join(&relpath).canonicalize()?; let mut path = self.directory.join(&relpath).canonicalize()?;
if path.is_dir() { if path.is_dir() {
if let Some(ref redir_index) = self.index { if let Some(redir_index) = C::index_file() {
// TODO: Don't redirect, just return the index content. path.push(redir_index);
// TODO: It'd be nice if there were a good usable URL manipulation } else if C::show_index() {
// library
let mut new_path: String = req.path().to_owned();
if !new_path.ends_with('/') {
new_path.push('/');
}
new_path.push_str(redir_index);
HttpResponse::Found()
.header(header::LOCATION, new_path.as_str())
.finish()
.respond_to(&req)
} else if self.show_index {
let dir = Directory::new(self.directory.clone(), path); let dir = Directory::new(self.directory.clone(), path);
Ok((*self.renderer)(&dir, &req)?.into()) return Ok(C::directory_listing(&dir, &req)?.into())
} else { } else {
Err(StaticFileError::IsDirectory.into()) return Err(StaticFileError::IsDirectory.into())
} }
} else { }
NamedFile::open_with_config(path, C::default())? NamedFile::open_with_config(path, C::default())?
.set_cpu_pool(self.cpu_pool.clone()) .set_cpu_pool(self.cpu_pool.clone())
.respond_to(&req)? .respond_to(&req)?
.respond_to(&req) .respond_to(&req)
} }
}
} }
impl<S: 'static, C: 'static + StaticFileConfig> Handler<S> for StaticFiles<S, C> { impl<S: 'static, C: 'static + StaticFileConfig> Handler<S> for StaticFiles<C> {
type Result = Result<AsyncResult<HttpResponse>, Error>; type Result = Result<AsyncResult<HttpResponse>, Error>;
fn handle(&self, req: &HttpRequest<S>) -> Self::Result { fn handle(&self, req: &HttpRequest<S>) -> Self::Result {
self.try_handle(req).or_else(|e| { self.try_handle(req).or_else(|e| {
debug!("StaticFiles: Failed to handle {}: {}", req.path(), e); debug!("StaticFiles: Failed to handle {}: {}", req.path(), e);
Ok(self.default.handle(req)) Ok(C::default_handler(req))
}) })
} }
} }
@ -1129,10 +1102,19 @@ mod tests {
#[test] #[test]
fn test_named_file_ranges_status_code() { fn test_named_file_ranges_status_code() {
#[derive(Default)]
struct IndexCfg;
impl StaticFileConfig for IndexCfg {
fn index_file() -> Option<&'static str> {
Some("Cargo.toml")
}
}
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
StaticFiles::new(".").unwrap().index_file("Cargo.toml"), StaticFiles::with_config(".", IndexCfg).unwrap(),
) )
}); });
@ -1160,12 +1142,19 @@ mod tests {
#[test] #[test]
fn test_named_file_content_range_headers() { fn test_named_file_content_range_headers() {
#[derive(Default)]
struct IndexCfg;
impl StaticFileConfig for IndexCfg {
fn index_file() -> Option<&'static str> {
Some("tests/test.binary")
}
}
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
StaticFiles::new(".") StaticFiles::with_config(".", IndexCfg)
.unwrap() .unwrap()
.index_file("tests/test.binary"),
) )
}); });
@ -1210,12 +1199,19 @@ mod tests {
#[test] #[test]
fn test_named_file_content_length_headers() { fn test_named_file_content_length_headers() {
#[derive(Default)]
struct IndexCfg;
impl StaticFileConfig for IndexCfg {
fn index_file() -> Option<&'static str> {
Some("tests/test.binary")
}
}
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
StaticFiles::new(".") StaticFiles::with_config(".", IndexCfg)
.unwrap() .unwrap()
.index_file("tests/test.binary"),
) )
}); });
@ -1355,7 +1351,16 @@ mod tests {
#[test] #[test]
fn test_static_files() { fn test_static_files() {
let mut st = StaticFiles::new(".").unwrap().show_files_listing(); #[derive(Default)]
struct ShowIndex;
impl StaticFileConfig for ShowIndex {
fn show_index() -> bool {
true
}
}
let st = StaticFiles::with_config(".", ShowIndex).unwrap();
let req = TestRequest::with_uri("/missing") let req = TestRequest::with_uri("/missing")
.param("tail", "missing") .param("tail", "missing")
.finish(); .finish();
@ -1363,7 +1368,7 @@ mod tests {
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::NOT_FOUND); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
st.show_index = false; let st = StaticFiles::new(".").unwrap();
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
@ -1371,7 +1376,7 @@ mod tests {
let req = TestRequest::default().param("tail", "").finish(); let req = TestRequest::default().param("tail", "").finish();
st.show_index = true; let st = StaticFiles::with_config(".", ShowIndex).unwrap();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!( assert_eq!(
@ -1384,18 +1389,24 @@ mod tests {
#[test] #[test]
fn test_static_files_bad_directory() { fn test_static_files_bad_directory() {
let st: Result<StaticFiles<()>, Error> = StaticFiles::new("missing"); let st: Result<StaticFiles, Error> = StaticFiles::new("missing");
assert!(st.is_err()); assert!(st.is_err());
let st: Result<StaticFiles<()>, Error> = StaticFiles::new("Cargo.toml"); let st: Result<StaticFiles, Error> = StaticFiles::new("Cargo.toml");
assert!(st.is_err()); assert!(st.is_err());
} }
#[test] #[test]
fn test_default_handler_file_missing() { fn test_default_handler_file_missing() {
let st = StaticFiles::new(".") #[derive(Default)]
.unwrap() struct DefaultHandler;
.default_handler(|_: &_| "default content"); impl StaticFileConfig for DefaultHandler {
fn default_handler<S>(_: &HttpRequest<S>) -> AsyncResult<HttpResponse> {
AsyncResult::ok("default content")
}
}
let st = StaticFiles::with_config(".", DefaultHandler)
.unwrap();
let req = TestRequest::with_uri("/missing") let req = TestRequest::with_uri("/missing")
.param("tail", "missing") .param("tail", "missing")
.finish(); .finish();
@ -1411,38 +1422,79 @@ mod tests {
#[test] #[test]
fn test_redirect_to_index() { fn test_redirect_to_index() {
let st = StaticFiles::new(".").unwrap().index_file("index.html"); #[derive(Default)]
struct IndexCfg;
impl StaticFileConfig for IndexCfg {
fn index_file() -> Option<&'static str> {
Some("test.png")
}
}
const EXPECTED_INDEX: &[u8] = include_bytes!("../tests/test.png");
let expected_len = format!("{}", EXPECTED_INDEX.len());
let st = StaticFiles::with_config(".", IndexCfg).unwrap();
let req = TestRequest::default().uri("/tests").finish(); let req = TestRequest::default().uri("/tests").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( let len = resp
resp.headers().get(header::LOCATION).unwrap(), .headers()
"/tests/index.html" .get(header::CONTENT_LENGTH)
); .unwrap()
.to_str()
.unwrap();
assert_eq!(len, expected_len);
let req = TestRequest::default().uri("/tests/").finish(); let req = TestRequest::default().uri("/tests/").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( let len = resp
resp.headers().get(header::LOCATION).unwrap(), .headers()
"/tests/index.html" .get(header::CONTENT_LENGTH)
); .unwrap()
.to_str()
.unwrap();
assert_eq!(len, expected_len);
} }
#[test] #[test]
fn test_redirect_to_index_nested() { fn test_redirect_to_index_nested() {
let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); #[derive(Default)]
struct IndexCfg;
impl StaticFileConfig for IndexCfg {
fn index_file() -> Option<&'static str> {
Some("mod.rs")
}
}
const EXPECTED_INDEX: &[u8] = include_bytes!("client/mod.rs");
let expected_len = format!("{}", EXPECTED_INDEX.len());
let st = StaticFiles::with_config(".", IndexCfg).unwrap();
let req = TestRequest::default().uri("/src/client").finish(); let req = TestRequest::default().uri("/src/client").finish();
let resp = st.handle(&req).respond_to(&req).unwrap(); let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = resp.as_msg(); let resp = resp.as_msg();
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
assert_eq!( let len = resp
resp.headers().get(header::LOCATION).unwrap(), .headers()
"/src/client/mod.rs" .get(header::CONTENT_LENGTH)
); .unwrap()
.to_str()
.unwrap();
assert_eq!(len, expected_len);
}
#[derive(Default)]
struct CargoTomlIndex;
impl StaticFileConfig for CargoTomlIndex {
fn index_file() -> Option<&'static str> {
Some("Cargo.toml")
}
} }
#[test] #[test]
@ -1450,30 +1502,16 @@ mod tests {
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new() App::new()
.prefix("public") .prefix("public")
.handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) .handler("/", StaticFiles::with_config(".", CargoTomlIndex).unwrap())
}); });
let request = srv.get().uri(srv.url("/public")).finish().unwrap(); let request = srv.get().uri(srv.url("/public")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let resp = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/public/Cargo.toml");
let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); let request = srv.get().uri(srv.url("/public/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let resp = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/public/Cargo.toml");
} }
#[test] #[test]
@ -1481,31 +1519,17 @@ mod tests {
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
StaticFiles::new(".").unwrap().index_file("Cargo.toml"), StaticFiles::with_config(".", CargoTomlIndex).unwrap(),
) )
}); });
let request = srv.get().uri(srv.url("/test")).finish().unwrap(); let request = srv.get().uri(srv.url("/test")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let resp = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/test/Cargo.toml");
let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
let response = srv.execute(request.send()).unwrap(); let resp = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::OK);
let loc = response
.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap();
assert_eq!(loc, "/test/Cargo.toml");
} }
#[test] #[test]
@ -1513,7 +1537,7 @@ mod tests {
let mut srv = test::TestServer::with_factory(|| { let mut srv = test::TestServer::with_factory(|| {
App::new().handler( App::new().handler(
"test", "test",
StaticFiles::new(".").unwrap().index_file("Cargo.toml"), StaticFiles::with_config(".", CargoTomlIndex).unwrap(),
) )
}); });
@ -1522,8 +1546,8 @@ mod tests {
.uri(srv.url("/test/%43argo.toml")) .uri(srv.url("/test/%43argo.toml"))
.finish() .finish()
.unwrap(); .unwrap();
let response = srv.execute(request.send()).unwrap(); let resp = srv.execute(request.send()).unwrap();
assert_eq!(response.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
struct T(&'static str, u64, Vec<HttpRange>); struct T(&'static str, u64, Vec<HttpRange>);