From 3751656722ca4f0a220a6555ec37d270e6b6d06c Mon Sep 17 00:00:00 2001 From: axon-q Date: Sat, 9 Jun 2018 11:20:06 +0000 Subject: [PATCH 1/5] expose fs::file_extension_to_mime() function --- src/fs.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/fs.rs b/src/fs.rs index a4418bce..30755778 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -29,6 +29,14 @@ use param::FromParam; /// Env variable for default cpu pool size for `StaticFiles` const ENV_CPU_POOL_VAR: &str = "ACTIX_FS_POOL"; +/// 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 +/// 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; responds with the Content-Type based on the /// file extension. #[derive(Debug)] @@ -692,6 +700,18 @@ mod tests { use http::{header, Method, StatusCode}; 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] fn test_named_file_text() { assert!(NamedFile::open("test--").is_err()); From 1fdf6d13be9037cf82b90305f43dc02b604780a1 Mon Sep 17 00:00:00 2001 From: axon-q Date: Sat, 9 Jun 2018 13:38:21 +0000 Subject: [PATCH 2/5] content_disposition: add doc example --- src/header/common/content_disposition.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index 93102d46..0edebfed 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -70,7 +70,7 @@ pub enum DispositionParam { /// ``` /// use actix_web::http::header::{ContentDisposition, DispositionType, DispositionParam, Charset}; /// -/// let cd = ContentDisposition { +/// let cd1 = ContentDisposition { /// disposition: DispositionType::Attachment, /// parameters: vec![DispositionParam::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 /// )] /// }; +/// +/// 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)] pub struct ContentDisposition { From 8681a346c62084e324fc460845a44945047ce43b Mon Sep 17 00:00:00 2001 From: axon-q Date: Sat, 9 Jun 2018 13:48:36 +0000 Subject: [PATCH 3/5] fs: refactor Content-Type and Content-Disposition handling --- src/fs.rs | 174 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 132 insertions(+), 42 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 30755778..fd8abfaf 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -37,12 +37,13 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { get_mime_type(ext) } -/// A file with an associated name; responds with the Content-Type based on the -/// file extension. +/// A file with an associated name. #[derive(Debug)] pub struct NamedFile { path: PathBuf, file: File, + content_type: mime::Mime, + content_disposition: header::ContentDisposition, md: Metadata, modified: Option, cpu_pool: Option, @@ -62,15 +63,48 @@ impl NamedFile { /// let file = NamedFile::open("foo.txt"); /// ``` pub fn open>(path: P) -> io::Result { - let file = File::open(path.as_ref())?; - let md = file.metadata()?; + use header::{ContentDisposition, DispositionType, DispositionParam}; 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; + let 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")), + }; + + content_type = guess_mime_type(&path); + let disposition_type = match content_type.type_() { + mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + _ => DispositionType::Attachment, + }; + content_disposition = ContentDisposition { + disposition: disposition_type, + parameters: vec![ + DispositionParam::Filename( + header::Charset::Ext("UTF-8".to_owned()), + None, + filename.as_bytes().to_vec(), + ) + ], + }; + } + + let file = File::open(&path)?; + let md = file.metadata()?; let modified = md.modified().ok(); let cpu_pool = None; let encoding = None; Ok(NamedFile { path, file, + content_type, + content_disposition, md, modified, cpu_pool, @@ -125,6 +159,27 @@ impl NamedFile { 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 #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { @@ -220,23 +275,10 @@ impl Responder for NamedFile { fn respond_to(self, req: &HttpRequest) -> Result { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); - resp.if_some(self.path().extension(), |ext, 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() - ), - ); - }); + + resp.set(header::ContentType(self.content_type.clone())); + resp.header("Content-Disposition", format!("{}", &self.content_disposition)); + if let Some(current_encoding) = self.encoding { resp.content_encoding(current_encoding); } @@ -289,23 +331,10 @@ impl Responder for NamedFile { resp.content_encoding(current_encoding); } - resp.if_some(self.path().extension(), |ext, 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() - ), - ); - }) + resp.set(header::ContentType(self.content_type.clone())); + resp.header("Content-Disposition", format!("{}", &self.content_disposition)); + + resp .if_some(last_modified, |lm, resp| { resp.set(header::LastModified(lm)); }) @@ -733,7 +762,32 @@ mod tests { ); assert_eq!( 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\"" ); } @@ -757,7 +811,43 @@ mod tests { ); assert_eq!( 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\"" ); } @@ -781,7 +871,7 @@ mod tests { ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=test.binary" + "attachment; filename=\"test.binary\"" ); } @@ -806,7 +896,7 @@ mod tests { ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=Cargo.toml" + "inline; filename=\"Cargo.toml\"" ); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } From fee203b4029a7c8a1878b88be2f7aacc6df5511d Mon Sep 17 00:00:00 2001 From: axon-q Date: Sat, 9 Jun 2018 14:02:05 +0000 Subject: [PATCH 4/5] update changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 9171a237..29046f8f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,12 @@ ### 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 multipart fields From aee24d4af027a6def09a6facf0f3765a9237efd2 Mon Sep 17 00:00:00 2001 From: axon-q Date: Sat, 9 Jun 2018 14:47:06 +0000 Subject: [PATCH 5/5] minor syntax changes --- src/fs.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index fd8abfaf..10145952 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -68,8 +68,7 @@ impl NamedFile { // Get the name of the file and use it to construct default Content-Type // and Content-Disposition values - let content_type; - let content_disposition; + let (content_type, content_disposition) = { let filename = match path.file_name() { Some(name) => name.to_string_lossy(), @@ -78,12 +77,12 @@ impl NamedFile { "Provided path has no filename")), }; - content_type = guess_mime_type(&path); - let disposition_type = match content_type.type_() { + let ct = guess_mime_type(&path); + let disposition_type = match ct.type_() { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; - content_disposition = ContentDisposition { + let cd = ContentDisposition { disposition: disposition_type, parameters: vec![ DispositionParam::Filename( @@ -93,7 +92,8 @@ impl NamedFile { ) ], }; - } + (ct, cd) + }; let file = File::open(&path)?; let md = file.metadata()?; @@ -275,9 +275,8 @@ impl Responder for NamedFile { fn respond_to(self, req: &HttpRequest) -> Result { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); - - resp.set(header::ContentType(self.content_type.clone())); - resp.header("Content-Disposition", format!("{}", &self.content_disposition)); + resp.set(header::ContentType(self.content_type.clone())) + .header("Content-Disposition", format!("{}", &self.content_disposition)); if let Some(current_encoding) = self.encoding { resp.content_encoding(current_encoding); @@ -327,13 +326,13 @@ impl Responder for NamedFile { }; 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 { resp.content_encoding(current_encoding); } - resp.set(header::ContentType(self.content_type.clone())); - resp.header("Content-Disposition", format!("{}", &self.content_disposition)); - resp .if_some(last_modified, |lm, resp| { resp.set(header::LastModified(lm));