mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 17:52:56 +01:00
Merge pull request #293 from axon-q/static-file-updates
Better Content-Type and Content-Disposition handling for static files
This commit is contained in:
commit
3788887c92
@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Add `.set_content_type()` and `.set_content_disposition()` methods
|
||||||
|
to `fs::NamedFile` to allow overriding the values inferred by default
|
||||||
|
|
||||||
|
* Add `fs::file_extension_to_mime()` helper function to get the MIME
|
||||||
|
type for a file extension
|
||||||
|
|
||||||
* Add `.content_disposition()` method to parse Content-Disposition of
|
* Add `.content_disposition()` method to parse Content-Disposition of
|
||||||
multipart fields
|
multipart fields
|
||||||
|
|
||||||
|
193
src/fs.rs
193
src/fs.rs
@ -29,12 +29,21 @@ use param::FromParam;
|
|||||||
/// Env variable for default cpu pool size for `StaticFiles`
|
/// Env variable for default cpu pool size for `StaticFiles`
|
||||||
const ENV_CPU_POOL_VAR: &str = "ACTIX_FS_POOL";
|
const ENV_CPU_POOL_VAR: &str = "ACTIX_FS_POOL";
|
||||||
|
|
||||||
/// A file with an associated name; responds with the Content-Type based on the
|
/// Return the MIME type associated with a filename extension (case-insensitive).
|
||||||
/// file extension.
|
/// If `ext` is empty or no associated type for the extension was found, returns
|
||||||
|
/// the type `application/octet-stream`.
|
||||||
|
#[inline]
|
||||||
|
pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
||||||
|
get_mime_type(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file with an associated name.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NamedFile {
|
pub struct NamedFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
file: File,
|
file: File,
|
||||||
|
content_type: mime::Mime,
|
||||||
|
content_disposition: header::ContentDisposition,
|
||||||
md: Metadata,
|
md: Metadata,
|
||||||
modified: Option<SystemTime>,
|
modified: Option<SystemTime>,
|
||||||
cpu_pool: Option<CpuPool>,
|
cpu_pool: Option<CpuPool>,
|
||||||
@ -54,15 +63,48 @@ 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> {
|
||||||
let file = File::open(path.as_ref())?;
|
use header::{ContentDisposition, DispositionType, DispositionParam};
|
||||||
let md = file.metadata()?;
|
|
||||||
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
|
||||||
|
// and Content-Disposition values
|
||||||
|
let (content_type, content_disposition) =
|
||||||
|
{
|
||||||
|
let filename = match path.file_name() {
|
||||||
|
Some(name) => name.to_string_lossy(),
|
||||||
|
None => return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"Provided path has no filename")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ct = guess_mime_type(&path);
|
||||||
|
let disposition_type = match ct.type_() {
|
||||||
|
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||||
|
_ => DispositionType::Attachment,
|
||||||
|
};
|
||||||
|
let cd = ContentDisposition {
|
||||||
|
disposition: disposition_type,
|
||||||
|
parameters: vec![
|
||||||
|
DispositionParam::Filename(
|
||||||
|
header::Charset::Ext("UTF-8".to_owned()),
|
||||||
|
None,
|
||||||
|
filename.as_bytes().to_vec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
};
|
||||||
|
(ct, cd)
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = File::open(&path)?;
|
||||||
|
let md = file.metadata()?;
|
||||||
let modified = md.modified().ok();
|
let modified = md.modified().ok();
|
||||||
let cpu_pool = None;
|
let cpu_pool = None;
|
||||||
let encoding = None;
|
let encoding = None;
|
||||||
Ok(NamedFile {
|
Ok(NamedFile {
|
||||||
path,
|
path,
|
||||||
file,
|
file,
|
||||||
|
content_type,
|
||||||
|
content_disposition,
|
||||||
md,
|
md,
|
||||||
modified,
|
modified,
|
||||||
cpu_pool,
|
cpu_pool,
|
||||||
@ -117,6 +159,27 @@ impl NamedFile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the MIME Content-Type for serving this file. By default
|
||||||
|
/// the Content-Type is inferred from the filename extension.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||||
|
self.content_type = mime_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the Content-Disposition for serving this file. This allows
|
||||||
|
/// changing the inline/attachment disposition as well as the filename
|
||||||
|
/// sent to the peer. By default the disposition is `inline` for text,
|
||||||
|
/// image, and video content types, and `attachment` otherwise, and
|
||||||
|
/// the filename is taken from the path provided in the `open` method
|
||||||
|
/// after converting it to UTF-8 using
|
||||||
|
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
|
||||||
|
#[inline]
|
||||||
|
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||||
|
self.content_disposition = cd;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set content encoding for serving this file
|
/// Set content encoding for serving this file
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||||
@ -212,23 +275,9 @@ impl Responder for NamedFile {
|
|||||||
fn respond_to<S>(self, req: &HttpRequest<S>) -> Result<HttpResponse, io::Error> {
|
fn respond_to<S>(self, req: &HttpRequest<S>) -> Result<HttpResponse, io::Error> {
|
||||||
if self.status_code != StatusCode::OK {
|
if self.status_code != StatusCode::OK {
|
||||||
let mut resp = HttpResponse::build(self.status_code);
|
let mut resp = HttpResponse::build(self.status_code);
|
||||||
resp.if_some(self.path().extension(), |ext, resp| {
|
resp.set(header::ContentType(self.content_type.clone()))
|
||||||
resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy())));
|
.header("Content-Disposition", format!("{}", &self.content_disposition));
|
||||||
}).if_some(self.path().file_name(), |file_name, resp| {
|
|
||||||
let mime_type = guess_mime_type(self.path());
|
|
||||||
let inline_or_attachment = match mime_type.type_() {
|
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => "inline",
|
|
||||||
_ => "attachment",
|
|
||||||
};
|
|
||||||
resp.header(
|
|
||||||
"Content-Disposition",
|
|
||||||
format!(
|
|
||||||
"{inline_or_attachment}; filename={filename}",
|
|
||||||
inline_or_attachment = inline_or_attachment,
|
|
||||||
filename = file_name.to_string_lossy()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
resp.content_encoding(current_encoding);
|
resp.content_encoding(current_encoding);
|
||||||
}
|
}
|
||||||
@ -277,27 +326,14 @@ impl Responder for NamedFile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut resp = HttpResponse::build(self.status_code);
|
let mut resp = HttpResponse::build(self.status_code);
|
||||||
|
resp.set(header::ContentType(self.content_type.clone()))
|
||||||
|
.header("Content-Disposition", format!("{}", &self.content_disposition));
|
||||||
|
|
||||||
if let Some(current_encoding) = self.encoding {
|
if let Some(current_encoding) = self.encoding {
|
||||||
resp.content_encoding(current_encoding);
|
resp.content_encoding(current_encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.if_some(self.path().extension(), |ext, resp| {
|
resp
|
||||||
resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy())));
|
|
||||||
}).if_some(self.path().file_name(), |file_name, resp| {
|
|
||||||
let mime_type = guess_mime_type(self.path());
|
|
||||||
let inline_or_attachment = match mime_type.type_() {
|
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => "inline",
|
|
||||||
_ => "attachment",
|
|
||||||
};
|
|
||||||
resp.header(
|
|
||||||
"Content-Disposition",
|
|
||||||
format!(
|
|
||||||
"{inline_or_attachment}; filename={filename}",
|
|
||||||
inline_or_attachment = inline_or_attachment,
|
|
||||||
filename = file_name.to_string_lossy()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.if_some(last_modified, |lm, resp| {
|
.if_some(last_modified, |lm, resp| {
|
||||||
resp.set(header::LastModified(lm));
|
resp.set(header::LastModified(lm));
|
||||||
})
|
})
|
||||||
@ -692,6 +728,18 @@ mod tests {
|
|||||||
use http::{header, Method, StatusCode};
|
use http::{header, Method, StatusCode};
|
||||||
use test::{self, TestRequest};
|
use test::{self, TestRequest};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_extension_to_mime() {
|
||||||
|
let m = file_extension_to_mime("jpg");
|
||||||
|
assert_eq!(m, mime::IMAGE_JPEG);
|
||||||
|
|
||||||
|
let m = file_extension_to_mime("invalid extension!!");
|
||||||
|
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
|
let m = file_extension_to_mime("");
|
||||||
|
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_text() {
|
fn test_named_file_text() {
|
||||||
assert!(NamedFile::open("test--").is_err());
|
assert!(NamedFile::open("test--").is_err());
|
||||||
@ -713,7 +761,32 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
"inline; filename=Cargo.toml"
|
"inline; filename=\"Cargo.toml\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_named_file_set_content_type() {
|
||||||
|
let mut file = NamedFile::open("Cargo.toml")
|
||||||
|
.unwrap()
|
||||||
|
.set_content_type(mime::TEXT_XML)
|
||||||
|
.set_cpu_pool(CpuPool::new(1));
|
||||||
|
{
|
||||||
|
file.file();
|
||||||
|
let _f: &File = &file;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let _f: &mut File = &mut file;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = file.respond_to(&HttpRequest::default()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"text/xml"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
|
"inline; filename=\"Cargo.toml\""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,7 +810,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
"inline; filename=test.png"
|
"inline; filename=\"test.png\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_named_file_image_attachment() {
|
||||||
|
use header::{ContentDisposition, DispositionType, DispositionParam};
|
||||||
|
let cd = ContentDisposition {
|
||||||
|
disposition: DispositionType::Attachment,
|
||||||
|
parameters: vec![
|
||||||
|
DispositionParam::Filename(
|
||||||
|
header::Charset::Ext("UTF-8".to_owned()),
|
||||||
|
None,
|
||||||
|
"test.png".as_bytes().to_vec(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut file = NamedFile::open("tests/test.png")
|
||||||
|
.unwrap()
|
||||||
|
.set_content_disposition(cd)
|
||||||
|
.set_cpu_pool(CpuPool::new(1));
|
||||||
|
{
|
||||||
|
file.file();
|
||||||
|
let _f: &File = &file;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let _f: &mut File = &mut file;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = file.respond_to(&HttpRequest::default()).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\""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,7 +870,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
"attachment; filename=test.binary"
|
"attachment; filename=\"test.binary\""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -786,7 +895,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
"inline; filename=Cargo.toml"
|
"inline; filename=\"Cargo.toml\""
|
||||||
);
|
);
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ pub enum DispositionParam {
|
|||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::http::header::{ContentDisposition, DispositionType, DispositionParam, Charset};
|
/// use actix_web::http::header::{ContentDisposition, DispositionType, DispositionParam, Charset};
|
||||||
///
|
///
|
||||||
/// let cd = ContentDisposition {
|
/// let cd1 = ContentDisposition {
|
||||||
/// disposition: DispositionType::Attachment,
|
/// disposition: DispositionType::Attachment,
|
||||||
/// parameters: vec![DispositionParam::Filename(
|
/// parameters: vec![DispositionParam::Filename(
|
||||||
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
|
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
|
||||||
@ -78,6 +78,15 @@ pub enum DispositionParam {
|
|||||||
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
|
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
|
||||||
/// )]
|
/// )]
|
||||||
/// };
|
/// };
|
||||||
|
///
|
||||||
|
/// let cd2 = ContentDisposition {
|
||||||
|
/// disposition: DispositionType::Inline,
|
||||||
|
/// parameters: vec![DispositionParam::Filename(
|
||||||
|
/// Charset::Ext("UTF-8".to_owned()),
|
||||||
|
/// None,
|
||||||
|
/// "\u{2764}".as_bytes().to_vec()
|
||||||
|
/// )]
|
||||||
|
/// };
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ContentDisposition {
|
pub struct ContentDisposition {
|
||||||
|
Loading…
Reference in New Issue
Block a user