1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-24 00:21:08 +01:00

Merge branch 'master' of github.com:actix/actix-web

This commit is contained in:
Nikolay Kim 2018-07-20 12:43:44 -07:00
commit 040d9d2755
3 changed files with 204 additions and 32 deletions

View File

@ -4,6 +4,9 @@
### Added ### Added
* Add `fs::StaticFileConfig` to provide means of customizing static file services. It allows to map
`mime` to `Content-Disposition`, specify whether to use `ETag` and `Last-Modified` and allowed methods.
* Add `.has_prefixed_resource()` method to `router::ResourceInfo` for route matching with prefix awareness * Add `.has_prefixed_resource()` method to `router::ResourceInfo` for route matching with prefix awareness
* Add `HttpMessage::readlines()` for reading line by line. * Add `HttpMessage::readlines()` for reading line by line.

231
src/fs.rs
View File

@ -6,6 +6,7 @@ use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use std::{cmp, io}; use std::{cmp, io};
use std::marker::PhantomData;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
@ -27,6 +28,73 @@ use httprequest::HttpRequest;
use httpresponse::HttpResponse; use httpresponse::HttpResponse;
use param::FromParam; use param::FromParam;
use server::settings::DEFAULT_CPUPOOL; use server::settings::DEFAULT_CPUPOOL;
use header::{ContentDisposition, DispositionParam, DispositionType};
///Describes `StaticFiles` configiration
///
///To configure actix's static resources you need
///to define own configiration type and implement any method
///you wish to customize.
///As trait implements reasonable defaults for Actix.
///
///## Example
///
///```rust
/// extern crate mime;
/// extern crate actix_web;
/// use actix_web::http::header::DispositionType;
/// use actix_web::fs::{StaticFileConfig, NamedFile};
///
/// #[derive(Default)]
/// struct MyConfig;
///
/// impl StaticFileConfig for MyConfig {
/// fn content_disposition_map(typ: mime::Name) -> DispositionType {
/// DispositionType::Attachment
/// }
/// }
///
/// let file = NamedFile::open_with_config("foo.txt", MyConfig);
///```
pub trait StaticFileConfig: Default {
///Describes mapping for mime type to content disposition header
///
///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
///Others are mapped to Attachment
fn content_disposition_map(typ: mime::Name) -> DispositionType {
match typ {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment,
}
}
///Describes whether Actix should attempt to calculate `ETag`
///
///Defaults to `true`
fn is_use_etag() -> bool {
true
}
///Describes whether Actix should use last modified date of file.
///
///Defaults to `true`
fn is_use_last_modifier() -> bool {
true
}
///Describes allowed methods to access static resources.
///
///By default all methods are allowed
fn is_method_allowed(_method: &Method) -> bool {
true
}
}
///Default content disposition as described in
///[StaticFileConfig](trait.StaticFileConfig.html)
#[derive(Default)]
pub struct DefaultConfig;
impl StaticFileConfig for DefaultConfig {}
/// Return the MIME type associated with a filename extension (case-insensitive). /// Return the MIME type associated with a filename extension (case-insensitive).
/// If `ext` is empty or no associated type for the extension was found, returns /// If `ext` is empty or no associated type for the extension was found, returns
@ -38,7 +106,7 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
/// A file with an associated name. /// A file with an associated name.
#[derive(Debug)] #[derive(Debug)]
pub struct NamedFile { pub struct NamedFile<C=DefaultConfig> {
path: PathBuf, path: PathBuf,
file: File, file: File,
content_type: mime::Mime, content_type: mime::Mime,
@ -47,8 +115,8 @@ pub struct NamedFile {
modified: Option<SystemTime>, modified: Option<SystemTime>,
cpu_pool: Option<CpuPool>, cpu_pool: Option<CpuPool>,
encoding: Option<ContentEncoding>, encoding: Option<ContentEncoding>,
only_get: bool,
status_code: StatusCode, status_code: StatusCode,
_cd_map: PhantomData<C>,
} }
impl NamedFile { impl NamedFile {
@ -62,7 +130,21 @@ impl NamedFile {
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
/// ``` /// ```
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> { pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
use header::{ContentDisposition, DispositionParam, DispositionType}; Self::open_with_config(path, DefaultConfig)
}
}
impl<C: StaticFileConfig> NamedFile<C> {
/// Attempts to open a file in read-only mode using provided configiration.
///
/// # Examples
///
/// ```rust
/// use actix_web::fs::{DefaultConfig, NamedFile};
///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
/// ```
pub fn open_with_config<P: AsRef<Path>>(path: P, _: C) -> io::Result<NamedFile<C>> {
let path = path.as_ref().to_path_buf(); let path = path.as_ref().to_path_buf();
// Get the name of the file and use it to construct default Content-Type // Get the name of the file and use it to construct default Content-Type
@ -79,10 +161,7 @@ impl NamedFile {
}; };
let ct = guess_mime_type(&path); let ct = guess_mime_type(&path);
let disposition_type = match ct.type_() { let disposition_type = C::content_disposition_map(ct.type_());
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment,
};
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: disposition_type, disposition: disposition_type,
parameters: vec![DispositionParam::Filename( parameters: vec![DispositionParam::Filename(
@ -108,18 +187,11 @@ impl NamedFile {
modified, modified,
cpu_pool, cpu_pool,
encoding, encoding,
only_get: false,
status_code: StatusCode::OK, status_code: StatusCode::OK,
_cd_map: PhantomData
}) })
} }
/// Allow only GET and HEAD methods
#[inline]
pub fn only_get(mut self) -> Self {
self.only_get = true;
self
}
/// Returns reference to the underlying `File` object. /// Returns reference to the underlying `File` object.
#[inline] #[inline]
pub fn file(&self) -> &File { pub fn file(&self) -> &File {
@ -218,7 +290,7 @@ impl NamedFile {
} }
} }
impl Deref for NamedFile { impl<C> Deref for NamedFile<C> {
type Target = File; type Target = File;
fn deref(&self) -> &File { fn deref(&self) -> &File {
@ -226,7 +298,7 @@ impl Deref for NamedFile {
} }
} }
impl DerefMut for NamedFile { impl<C> DerefMut for NamedFile<C> {
fn deref_mut(&mut self) -> &mut File { fn deref_mut(&mut self) -> &mut File {
&mut self.file &mut self.file
} }
@ -267,7 +339,7 @@ fn none_match<S>(etag: Option<&header::EntityTag>, req: &HttpRequest<S>) -> bool
} }
} }
impl Responder for NamedFile { impl<C: StaticFileConfig> Responder for NamedFile<C> {
type Item = HttpResponse; type Item = HttpResponse;
type Error = io::Error; type Error = io::Error;
@ -294,7 +366,7 @@ impl Responder for NamedFile {
return Ok(resp.streaming(reader)); return Ok(resp.streaming(reader));
} }
if self.only_get && *req.method() != Method::GET && *req.method() != Method::HEAD if !C::is_method_allowed(req.method())
{ {
return Ok(HttpResponse::MethodNotAllowed() return Ok(HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain") .header(header::CONTENT_TYPE, "text/plain")
@ -302,8 +374,14 @@ impl Responder for NamedFile {
.body("This resource only supports GET and HEAD.")); .body("This resource only supports GET and HEAD."));
} }
let etag = self.etag(); let etag = match C::is_use_etag() {
let last_modified = self.last_modified(); true => self.etag(),
false => None,
};
let last_modified = match C::is_use_last_modifier() {
true => self.last_modified(),
false => None,
};
// check preconditions // check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) { let precondition_failed = if !any_match(etag.as_ref(), req) {
@ -559,7 +637,7 @@ fn directory_listing<S>(
/// .finish(); /// .finish();
/// } /// }
/// ``` /// ```
pub struct StaticFiles<S> { pub struct StaticFiles<S, C=DefaultConfig> {
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
@ -568,6 +646,7 @@ pub struct StaticFiles<S> {
renderer: Box<DirectoryRenderer<S>>, renderer: Box<DirectoryRenderer<S>>,
_chunk_size: usize, _chunk_size: usize,
_follow_symlinks: bool, _follow_symlinks: bool,
_cd_map: PhantomData<C>,
} }
impl<S: 'static> StaticFiles<S> { impl<S: 'static> StaticFiles<S> {
@ -577,10 +656,7 @@ impl<S: 'static> StaticFiles<S> {
/// 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<S>, Error> {
// use default CpuPool Self::with_config(dir, DefaultConfig)
let pool = { DEFAULT_CPUPOOL.lock().clone() };
StaticFiles::with_pool(dir, pool)
} }
/// Create new `StaticFiles` instance for specified base directory and /// Create new `StaticFiles` instance for specified base directory and
@ -588,6 +664,26 @@ impl<S: 'static> StaticFiles<S> {
pub fn with_pool<T: Into<PathBuf>>( pub fn with_pool<T: Into<PathBuf>>(
dir: T, pool: CpuPool, dir: T, pool: CpuPool,
) -> Result<StaticFiles<S>, Error> { ) -> Result<StaticFiles<S>, Error> {
Self::with_config_pool(dir, pool, DefaultConfig)
}
}
impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
/// Create new `StaticFiles` instance for specified base directory.
///
/// Identical with `new` but allows to specify configiration to use.
pub fn with_config<T: Into<PathBuf>>(dir: T, config: C) -> Result<StaticFiles<S, C>, Error> {
// use default CpuPool
let pool = { DEFAULT_CPUPOOL.lock().clone() };
StaticFiles::with_config_pool(dir, pool, config)
}
/// Create new `StaticFiles` instance for specified base directory with config and
/// `CpuPool`.
pub fn with_config_pool<T: Into<PathBuf>>(
dir: T, pool: CpuPool, _: C
) -> Result<StaticFiles<S, C>, Error> {
let dir = dir.into().canonicalize()?; let dir = dir.into().canonicalize()?;
if !dir.is_dir() { if !dir.is_dir() {
@ -605,6 +701,7 @@ impl<S: 'static> StaticFiles<S> {
renderer: Box::new(directory_listing), renderer: Box::new(directory_listing),
_chunk_size: 0, _chunk_size: 0,
_follow_symlinks: false, _follow_symlinks: false,
_cd_map: PhantomData
}) })
} }
@ -631,13 +728,13 @@ impl<S: 'static> StaticFiles<S> {
/// ///
/// Redirects to specific index file for directory "/" instead of /// Redirects to specific index file for directory "/" instead of
/// showing files listing. /// showing files listing.
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S> { pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> {
self.index = Some(index.into()); self.index = Some(index.into());
self self
} }
/// Sets default handler which is used when no matched file could be found. /// 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> { pub fn default_handler<H: Handler<S>>(mut self, handler: H) -> StaticFiles<S, C> {
self.default = Box::new(WrapHandler::new(handler)); self.default = Box::new(WrapHandler::new(handler));
self self
} }
@ -672,7 +769,7 @@ impl<S: 'static> StaticFiles<S> {
Err(StaticFileError::IsDirectory.into()) Err(StaticFileError::IsDirectory.into())
} }
} else { } else {
NamedFile::open(path)? 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)
@ -680,7 +777,7 @@ impl<S: 'static> StaticFiles<S> {
} }
} }
impl<S: 'static> Handler<S> for StaticFiles<S> { impl<S: 'static, C: 'static + StaticFileConfig> Handler<S> for StaticFiles<S, 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 {
@ -920,6 +1017,56 @@ mod tests {
); );
} }
#[derive(Default)]
pub struct AllAttachmentConfig;
impl StaticFileConfig for AllAttachmentConfig {
fn content_disposition_map(_typ: mime::Name) -> DispositionType {
DispositionType::Attachment
}
}
#[derive(Default)]
pub struct AllInlineConfig;
impl StaticFileConfig for AllInlineConfig {
fn content_disposition_map(_typ: mime::Name) -> DispositionType {
DispositionType::Inline
}
}
#[test]
fn test_named_file_image_attachment_and_custom_config() {
let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig)
.unwrap()
.set_cpu_pool(CpuPool::new(1));
let req = TestRequest::default().finish();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"image/png"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"attachment; filename=\"test.png\""
);
let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig)
.unwrap()
.set_cpu_pool(CpuPool::new(1));
let req = TestRequest::default().finish();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"image/png"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.png\""
);
}
#[test] #[test]
fn test_named_file_binary() { fn test_named_file_binary() {
let mut file = NamedFile::open("tests/test.binary") let mut file = NamedFile::open("tests/test.binary")
@ -1143,12 +1290,32 @@ mod tests {
assert_eq!(bytes, data); assert_eq!(bytes, data);
} }
#[derive(Default)]
pub struct OnlyMethodHeadConfig;
impl StaticFileConfig for OnlyMethodHeadConfig {
fn is_method_allowed(method: &Method) -> bool {
match *method {
Method::HEAD => true,
_ => false
}
}
}
#[test] #[test]
fn test_named_file_not_allowed() { fn test_named_file_not_allowed() {
let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
let req = TestRequest::default().method(Method::POST).finish(); let req = TestRequest::default().method(Method::POST).finish();
let file = NamedFile::open("Cargo.toml").unwrap(); let resp = file.respond_to(&req).unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let resp = file.only_get().respond_to(&req).unwrap(); let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
let req = TestRequest::default().method(Method::PUT).finish();
let resp = file.respond_to(&req).unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
let req = TestRequest::default().method(Method::GET).finish();
let resp = file.respond_to(&req).unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
} }

View File

@ -436,6 +436,7 @@ mod tests {
router.register_resource(resource); router.register_resource(resource);
let info = router.default_route_info(); let info = router.default_route_info();
assert!(!info.has_prefixed_resource("/use/"));
assert!(info.has_resource("/user/test.html")); assert!(info.has_resource("/user/test.html"));
assert!(info.has_prefixed_resource("/user/test.html")); assert!(info.has_prefixed_resource("/user/test.html"));
assert!(!info.has_resource("/test/unknown")); assert!(!info.has_resource("/test/unknown"));
@ -468,6 +469,7 @@ mod tests {
let mut info = router.default_route_info(); let mut info = router.default_route_info();
info.set_prefix(7); info.set_prefix(7);
assert!(!info.has_prefixed_resource("/use/"));
assert!(info.has_resource("/user/test.html")); assert!(info.has_resource("/user/test.html"));
assert!(!info.has_prefixed_resource("/user/test.html")); assert!(!info.has_prefixed_resource("/user/test.html"));
assert!(!info.has_resource("/prefix/user/test.html")); assert!(!info.has_resource("/prefix/user/test.html"));