1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-12-18 01:43:58 +01:00

add files preference for utf8 text responses (#1714)

This commit is contained in:
Rob Ede 2020-10-06 21:56:28 +01:00 committed by GitHub
parent c2c71cc626
commit 26c1a901d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 41 deletions

View File

@ -1,6 +1,14 @@
# Changes # Changes
## [Unreleased] - 2020-xx-xx ## [Unreleased] - 2020-xx-xx
* Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
[#1714]: https://github.com/actix/actix-web/pull/1714
## [0.3.0] - 2020-09-11
* No significant changes from 0.3.0-beta.1.
## [0.3.0-beta.1] - 2020-07-15 ## [0.3.0-beta.1] - 2020-07-15
* Update `v_htmlescape` to 0.10 * Update `v_htmlescape` to 0.10

View File

@ -0,0 +1,52 @@
use mime::Mime;
/// Transforms MIME `text/*` types into their UTF-8 equivalent, if supported.
///
/// MIME types that are converted
/// - application/javascript
/// - text/html
/// - text/css
/// - text/plain
/// - text/csv
/// - text/tab-separated-values
pub(crate) fn equiv_utf8_text(ct: Mime) -> Mime {
// use (roughly) order of file-type popularity for a web server
if ct == mime::APPLICATION_JAVASCRIPT {
return mime::APPLICATION_JAVASCRIPT_UTF_8;
}
if ct == mime::TEXT_HTML {
return mime::TEXT_HTML_UTF_8;
}
if ct == mime::TEXT_CSS {
return mime::TEXT_CSS_UTF_8;
}
if ct == mime::TEXT_PLAIN {
return mime::TEXT_PLAIN_UTF_8;
}
if ct == mime::TEXT_CSV {
return mime::TEXT_CSV_UTF_8;
}
if ct == mime::TEXT_TAB_SEPARATED_VALUES {
return mime::TEXT_TAB_SEPARATED_VALUES_UTF_8;
}
ct
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equiv_utf8_text() {
assert_eq!(equiv_utf8_text(mime::TEXT_PLAIN), mime::TEXT_PLAIN_UTF_8);
assert_eq!(equiv_utf8_text(mime::TEXT_XML), mime::TEXT_XML);
assert_eq!(equiv_utf8_text(mime::IMAGE_PNG), mime::IMAGE_PNG);
}
}

View File

@ -138,24 +138,33 @@ impl Files {
self self
} }
#[inline]
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value); self.file_flags.set(named::Flags::ETAG, value);
self self
} }
#[inline]
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value); self.file_flags.set(named::Flags::LAST_MD, value);
self self
} }
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value);
self
}
/// Specifies custom guards to use for directory listings and files. /// Specifies custom guards to use for directory listings and files.
/// ///
/// Default behaviour allows GET and HEAD. /// Default behaviour allows GET and HEAD.

View File

@ -8,12 +8,8 @@
//! use actix_files::Files; //! use actix_files::Files;
//! //!
//! let app = App::new() //! let app = App::new()
//! .service(Files::new("/static", ".")); //! .service(Files::new("/static", ".").prefer_utf8(true));
//! ``` //! ```
//!
//! # Implementation Quirks
//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8`
//! extension on the Content-Type header.
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
@ -30,6 +26,7 @@ use mime_guess::from_ext;
mod chunked; mod chunked;
mod directory; mod directory;
mod encoding;
mod error; mod error;
mod files; mod files;
mod named; mod named;
@ -93,6 +90,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_file_extension_to_mime() { async fn test_file_extension_to_mime() {
let m = file_extension_to_mime("");
assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
let m = file_extension_to_mime("jpg"); let m = file_extension_to_mime("jpg");
assert_eq!(m, mime::IMAGE_JPEG); assert_eq!(m, mime::IMAGE_JPEG);

View File

@ -22,20 +22,21 @@ use bitflags::bitflags;
use futures_util::future::{ready, Ready}; use futures_util::future::{ready, Ready};
use mime_guess::from_path; use mime_guess::from_path;
use crate::range::HttpRange;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! { bitflags! {
pub(crate) struct Flags: u8 { pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001; const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010; const LAST_MD = 0b0000_0010;
const CONTENT_DISPOSITION = 0b0000_0100; const CONTENT_DISPOSITION = 0b0000_0100;
const PREFER_UTF8 = 0b0000_1000;
} }
} }
impl Default for Flags { impl Default for Flags {
fn default() -> Self { fn default() -> Self {
Flags::all() Flags::from_bits_truncate(0b0000_0111)
} }
} }
@ -92,6 +93,7 @@ impl NamedFile {
}; };
let ct = from_path(&path).first_or_octet_stream(); let ct = from_path(&path).first_or_octet_stream();
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
@ -215,24 +217,33 @@ impl NamedFile {
self self
} }
#[inline]
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.flags.set(Flags::ETAG, value); self.flags.set(Flags::ETAG, value);
self self
} }
#[inline]
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.flags.set(Flags::LAST_MD, value); self.flags.set(Flags::LAST_MD, value);
self self
} }
/// Specifies whether text responses should signal a UTF-8 encoding.
///
/// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self {
self.flags.set(Flags::PREFER_UTF8, value);
self
}
pub(crate) fn etag(&self) -> Option<header::EntityTag> { pub(crate) fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's. // This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| { self.modified.as_ref().map(|mtime| {
@ -268,18 +279,24 @@ impl NamedFile {
/// Creates an `HttpResponse` with file as a streaming body. /// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> { pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone())) if self.flags.contains(Flags::PREFER_UTF8) {
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { let ct = equiv_utf8_text(self.content_type.clone());
res.header(header::CONTENT_TYPE, ct.to_string());
} else {
res.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header( res.header(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); );
}); }
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = ChunkedReadFile { let reader = ChunkedReadFile {
@ -290,7 +307,7 @@ impl NamedFile {
counter: 0, counter: 0,
}; };
return Ok(resp.streaming(reader)); return Ok(res.streaming(reader));
} }
let etag = if self.flags.contains(Flags::ETAG) { let etag = if self.flags.contains(Flags::ETAG) {
@ -342,25 +359,33 @@ impl 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()))
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { if self.flags.contains(Flags::PREFER_UTF8) {
res.header( let ct = equiv_utf8_text(self.content_type.clone());
resp.header(header::CONTENT_TYPE, ct.to_string());
} else {
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.header(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); );
}); }
// default compressing // default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); resp.encoding(current_encoding);
} }
resp.if_some(last_modified, |lm, resp| { if let Some(lm) = last_modified {
resp.set(header::LastModified(lm)); resp.header(header::LAST_MODIFIED, lm.to_string());
}) }
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag)); if let Some(etag) = etag {
}); resp.header(header::ETAG, etag.to_string());
}
resp.header(header::ACCEPT_RANGES, "bytes"); resp.header(header::ACCEPT_RANGES, "bytes");

View File

@ -0,0 +1,40 @@
use actix_files::Files;
use actix_web::{
http::{
header::{self, HeaderValue},
StatusCode,
},
test::{self, TestRequest},
App,
};
#[actix_rt::test]
async fn test_utf8_file_contents() {
// use default ISO-8859-1 encoding
let mut srv =
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain")),
);
// prefer UTF-8 encoding
let mut srv = test::init_service(
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
)
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE),
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
);
}

View File

@ -0,0 +1,3 @@
中文内容显示正确。
English is OK.

View File

@ -1 +0,0 @@
1.42.0