Merge pull request #99 from actix/master

-
This commit is contained in:
云上于天 2020-10-09 18:19:07 +08:00 committed by GitHub
commit a347394858
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 224 additions and 78 deletions

View File

@ -1,12 +1,24 @@
# Changes # Changes
## [Unreleased] - 2020-xx-xx ## Unreleased - 2020-xx-xx
## [0.3.0-beta.1] - 2020-07-15
## 0.4.0 - 2020-10-06
* 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
* Update `v_htmlescape` to 0.10 * Update `v_htmlescape` to 0.10
* Update `actix-web` and `actix-http` dependencies to beta.1 * Update `actix-web` and `actix-http` dependencies to beta.1
## [0.3.0-alpha.1] - 2020-05-23
## 0.3.0-alpha.1 - 2020-05-23
* Update `actix-web` and `actix-http` dependencies to alpha * Update `actix-web` and `actix-http` dependencies to alpha
* Fix some typos in the docs * Fix some typos in the docs
* Bump minimum supported Rust version to 1.40 * Bump minimum supported Rust version to 1.40
@ -14,77 +26,73 @@
[#1384]: https://github.com/actix/actix-web/pull/1384 [#1384]: https://github.com/actix/actix-web/pull/1384
## [0.2.1] - 2019-12-22
## 0.2.1 - 2019-12-22
* Use the same format for file URLs regardless of platforms * Use the same format for file URLs regardless of platforms
## [0.2.0] - 2019-12-20
## 0.2.0 - 2019-12-20
* Fix BodyEncoding trait import #1220 * Fix BodyEncoding trait import #1220
## [0.2.0-alpha.1] - 2019-12-07
## 0.2.0-alpha.1 - 2019-12-07
* Migrate to `std::future` * Migrate to `std::future`
## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.7 - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of
## [0.1.6] - 2019-10-14 `actix_files::NamedFile` to be more compatible. (#1151)
## 0.1.6 - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132 * Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
## 0.1.5 - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1 * Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1 * Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113 * Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20
## 0.1.4 - 2019-07-20
* Allow to disable `Content-Disposition` header #686 * Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28
## 0.1.3 - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930 * Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13
## 0.1.2 - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914 * Content-Length is 0 for NamedFile HEAD request #914
* Fix ring dependency from actix-web default features for #741 * Fix ring dependency from actix-web default features for #741
## [0.1.1] - 2019-06-01
## 0.1.1 - 2019-06-01
* Static files are incorrectly served as both chunked and with length #812 * Static files are incorrectly served as both chunked and with length #812
## [0.1.0] - 2019-05-25
* NamedFile last-modified check always fails due to nano-seconds ## 0.1.0 - 2019-05-25
in file modified date #820 * NamedFile last-modified check always fails due to nano-seconds in file modified date #820
## [0.1.0-beta.4] - 2019-05-12
## 0.1.0-beta.4 - 2019-05-12
* Update actix-web to beta.4 * Update actix-web to beta.4
## [0.1.0-beta.1] - 2019-04-20
## 0.1.0-beta.1 - 2019-04-20
* Update actix-web to beta.1 * Update actix-web to beta.1
## [0.1.0-alpha.6] - 2019-04-14
## 0.1.0-alpha.6 - 2019-04-14
* Update actix-web to alpha6 * Update actix-web to alpha6
## [0.1.0-alpha.4] - 2019-04-08
## 0.1.0-alpha.4 - 2019-04-08
* Update actix-web to alpha4 * Update actix-web to alpha4
## [0.1.0-alpha.2] - 2019-04-02
## 0.1.0-alpha.2 - 2019-04-02
* Add default handler support * Add default handler support
## [0.1.0-alpha.1] - 2019-03-28
## 0.1.0-alpha.1 - 2019-03-28
* Initial impl * Initial impl

View File

@ -1,8 +1,8 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.3.0" version = "0.4.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for actix web." description = "Static file serving for Actix Web"
readme = "README.md" readme = "README.md"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"

View File

@ -1,9 +1,19 @@
# Static files support for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-files)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # actix-files
## Documentation & community resources > Static file serving for Actix Web
* [User Guide](https://actix.rs/docs/) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-files)
* [API Documentation](https://docs.rs/actix-files/) [![Documentation](https://docs.rs/actix-files/badge.svg)](https://docs.rs/actix-files)
* [Chat on gitter](https://gitter.im/actix/actix) [![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
* Cargo package: [actix-files](https://crates.io/crates/actix-files) ![License](https://img.shields.io/crates/l/actix-files.svg)
* Minimum supported Rust version: 1.40 or later <br />
[![dependency status](https://deps.rs/crate/actix-files/0.4.0/status.svg)](https://deps.rs/crate/actix-files/0.4.0)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.42 or later

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

@ -1,4 +1,4 @@
//! Static files support for Actix Web. //! Static file serving for Actix Web.
//! //!
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
@ -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( res.header(header::CONTENT_TYPE, ct.to_string());
header::CONTENT_DISPOSITION, } else {
self.content_disposition.to_string(), res.header(header::CONTENT_TYPE, self.content_type.to_string());
); }
});
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.header(
header::CONTENT_DISPOSITION,
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());
header::CONTENT_DISPOSITION, resp.header(header::CONTENT_TYPE, ct.to_string());
self.content_disposition.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,
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