migrate files

This commit is contained in:
Nikolay Kim 2020-04-11 11:57:19 +06:00
parent 09598fa723
commit 3b1618604e
4 changed files with 385 additions and 271 deletions

View file

@ -18,11 +18,12 @@ name = "ntex_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
ntex = { version = "0.1.7", default-features = false } ntex = "0.1.7"
bitflags = "1" bitflags = "1"
bytes = "0.5.4" bytes = "0.5.4"
futures = "0.3.4" futures = "0.3.4"
derive_more = "0.99.5" derive_more = "0.99.5"
hyperx = "1.0.0"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.1" mime_guess = "2.0.1"
@ -30,4 +31,4 @@ percent-encoding = "2.1"
v_htmlescape = "0.4" v_htmlescape = "0.4"
[dev-dependencies] [dev-dependencies]
ntex = { version = "0.1.7", features=["openssl"] } ntex = { version = "0.1.7", features=["openssl", "compress"] }

View file

@ -1,23 +1,35 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use derive_more::{Display, From};
use derive_more::Display; use ntex::http::StatusCode;
use ntex::web::error::{DefaultError, WebResponseError};
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
#[derive(Display, Debug, PartialEq)] #[derive(Display, Debug, From)]
pub enum FilesError { pub enum FilesError {
/// Path is not a directory /// Path is not a directory
#[allow(dead_code)] #[allow(dead_code)]
#[display(fmt = "Path is not a directory. Unable to serve static files")] #[display(fmt = "Path is not a directory. Unable to serve static files.")]
IsNotDirectory, IsNotDirectory,
/// Cannot render directory /// Cannot render directory
#[display(fmt = "Unable to render directory without index file")] #[display(fmt = "Unable to render directory without index file.")]
IsDirectory, IsDirectory,
/// Only GET and HEAD methods are allowed
#[display(fmt = "Request did not meet this resource's requirements.")]
MethodNotAllowed,
/// IO Error
#[display(fmt = "Error reading: {}", _0)]
Io(std::io::Error),
} }
/// Return `NotFound` for `FilesError` /// Return `NotFound` for `FilesError`
impl ResponseError for FilesError { impl WebResponseError<DefaultError> for FilesError {
fn error_response(&self) -> HttpResponse { fn status_code(&self) -> StatusCode {
HttpResponse::new(StatusCode::NOT_FOUND) match self {
FilesError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
_ => StatusCode::NOT_FOUND,
}
} }
} }
@ -35,7 +47,7 @@ pub enum UriSegmentError {
} }
/// Return `BadRequest` for `UriSegmentError` /// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError { impl WebResponseError<DefaultError> for UriSegmentError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
use std::convert::Infallible;
use std::error::Error;
use std::fs::{File, Metadata}; use std::fs::{File, Metadata};
use std::io; use std::io;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -11,14 +13,16 @@ use bitflags::bitflags;
use mime; use mime;
use mime_guess::from_path; use mime_guess::from_path;
use actix_http::body::SizedStream;
use actix_web::dev::BodyEncoding;
use actix_web::http::header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
};
use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
use futures::future::{ready, Ready}; use futures::future::{ready, Ready};
use futures::stream::TryStreamExt;
use hyperx::header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, Header,
};
use ntex::http::body::SizedStream;
use ntex::http::header::ContentEncoding;
use ntex::http::{self, StatusCode};
use ntex::web::BodyEncoding;
use ntex::web::{HttpRequest, HttpResponse, Responder};
use crate::range::HttpRange; use crate::range::HttpRange;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
@ -60,7 +64,7 @@ impl NamedFile {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// use actix_files::NamedFile; /// use ntex_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
/// use std::fs::File; /// use std::fs::File;
@ -94,15 +98,11 @@ impl NamedFile {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
let mut parameters = let parameters = vec![DispositionParam::Filename(
vec![DispositionParam::Filename(String::from(filename.as_ref()))]; Charset::Ext(String::from("UTF-8")),
if !filename.is_ascii() { None,
parameters.push(DispositionParam::FilenameExt(ExtendedValue { filename.into_owned().into_bytes(),
charset: Charset::Ext(String::from("UTF-8")), )];
language_tag: None,
value: filename.into_owned().into_bytes(),
}))
}
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: disposition_type, disposition: disposition_type,
parameters: parameters, parameters: parameters,
@ -131,7 +131,7 @@ impl NamedFile {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// use actix_files::NamedFile; /// use ntex_files::NamedFile;
/// ///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
/// ``` /// ```
@ -150,10 +150,9 @@ impl NamedFile {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use std::io; /// use ntex_files::NamedFile;
/// use actix_files::NamedFile;
/// ///
/// # fn path() -> io::Result<()> { /// # fn path() -> std::io::Result<()> {
/// let file = NamedFile::open("test.txt")?; /// let file = NamedFile::open("test.txt")?;
/// assert_eq!(file.path().as_os_str(), "foo.txt"); /// assert_eq!(file.path().as_os_str(), "foo.txt");
/// # Ok(()) /// # Ok(())
@ -257,13 +256,13 @@ impl NamedFile {
self.modified.map(|mtime| mtime.into()) self.modified.map(|mtime| mtime.into())
} }
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> { pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Infallible> {
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.set(header::ContentType(self.content_type.clone())) resp.header(http::header::CONTENT_TYPE, self.content_type.to_string())
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header( res.header(
header::CONTENT_DISPOSITION, http::header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); );
}); });
@ -294,9 +293,19 @@ impl NamedFile {
// check preconditions // check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) { let precondition_failed = if !any_match(etag.as_ref(), req) {
true true
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = {
(last_modified, req.get_header()) let mut header = None;
{ for hdr in req.headers().get_all(http::header::IF_UNMODIFIED_SINCE) {
if let Ok(v) = header::IfUnmodifiedSince::parse_header(
&header::Raw::from(hdr.as_bytes()),
) {
header = Some(v);
break;
}
}
(last_modified, header)
} {
let t1: SystemTime = m.clone().into(); let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
@ -310,11 +319,20 @@ impl NamedFile {
// check last modified // check last modified
let not_modified = if !none_match(etag.as_ref(), req) { let not_modified = if !none_match(etag.as_ref(), req) {
true true
} else if req.headers().contains_key(&header::IF_NONE_MATCH) { } else if req.headers().contains_key(&http::header::IF_NONE_MATCH) {
false false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = {
(last_modified, req.get_header()) let mut header = None;
{ for hdr in req.headers().get_all(http::header::IF_MODIFIED_SINCE) {
if let Ok(v) = header::IfModifiedSince::parse_header(&header::Raw::from(
hdr.as_bytes(),
)) {
header = Some(v);
break;
}
}
(last_modified, header)
} {
let t1: SystemTime = m.clone().into(); let t1: SystemTime = m.clone().into();
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
@ -326,10 +344,10 @@ 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())) resp.header(http::header::CONTENT_TYPE, self.content_type.to_string())
.if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
res.header( res.header(
header::CONTENT_DISPOSITION, http::header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
); );
}); });
@ -339,26 +357,29 @@ impl NamedFile {
} }
resp.if_some(last_modified, |lm, resp| { resp.if_some(last_modified, |lm, resp| {
resp.set(header::LastModified(lm)); resp.header(
http::header::LAST_MODIFIED,
header::LastModified(lm).to_string(),
);
}) })
.if_some(etag, |etag, resp| { .if_some(etag, |etag, resp| {
resp.set(header::ETag(etag)); resp.header(http::header::ETAG, header::ETag(etag).to_string());
}); });
resp.header(header::ACCEPT_RANGES, "bytes"); resp.header(http::header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len(); let mut length = self.md.len();
let mut offset = 0; let mut offset = 0;
// check for range header // check for range header
if let Some(ranges) = req.headers().get(&header::RANGE) { if let Some(ranges) = req.headers().get(&http::header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() { if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length; length = rangesvec[0].length;
offset = rangesvec[0].start; offset = rangesvec[0].start;
resp.encoding(ContentEncoding::Identity); resp.encoding(ContentEncoding::Identity);
resp.header( resp.header(
header::CONTENT_RANGE, http::header::CONTENT_RANGE,
format!( format!(
"bytes {}-{}/{}", "bytes {}-{}/{}",
offset, offset,
@ -367,7 +388,10 @@ impl NamedFile {
), ),
); );
} else { } else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); resp.header(
http::header::CONTENT_RANGE,
format!("bytes */{}", length),
);
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
}; };
} else { } else {
@ -391,7 +415,13 @@ impl NamedFile {
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)) Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader))
} else { } else {
Ok(resp.body(SizedStream::new(length, reader))) Ok(resp.body(SizedStream::new(
length,
reader.map_err(|e| {
let e: Box<dyn Error> = Box::new(e);
e
}),
)))
} }
} }
} }
@ -412,42 +442,51 @@ impl DerefMut for NamedFile {
/// Returns true if `req` has no `If-Match` header or one which matches `etag`. /// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() { if let Some(val) = req.headers().get(http::header::IF_MATCH) {
None | Some(header::IfMatch::Any) => true, if let Ok(val) = header::IfMatch::parse_header(&val) {
Some(header::IfMatch::Items(ref items)) => { match val {
if let Some(some_etag) = etag { header::IfMatch::Any => return true,
for item in items { header::IfMatch::Items(ref items) => {
if item.strong_eq(some_etag) { if let Some(some_etag) = etag {
return true; for item in items {
if item.strong_eq(some_etag) {
return true;
}
}
} }
} }
} };
false return false;
} }
} }
true
} }
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. /// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() { if let Some(val) = req.headers().get(http::header::IF_NONE_MATCH) {
Some(header::IfNoneMatch::Any) => false, if let Ok(val) = header::IfNoneMatch::parse_header(&val) {
Some(header::IfNoneMatch::Items(ref items)) => { return match val {
if let Some(some_etag) = etag { header::IfNoneMatch::Any => false,
for item in items { header::IfNoneMatch::Items(ref items) => {
if item.weak_eq(some_etag) { if let Some(some_etag) = etag {
return false; for item in items {
if item.weak_eq(some_etag) {
return false;
}
}
} }
true
} }
} };
true
} }
None => true,
} }
true
} }
impl Responder for NamedFile { impl<Err> Responder<Err> for NamedFile {
type Error = Error; type Error = Infallible;
type Future = Ready<Result<HttpResponse, Error>>; type Future = Ready<Result<HttpResponse, Infallible>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future { fn respond_to(self, req: &HttpRequest) -> Self::Future {
ready(self.into_response(req)) ready(self.into_response(req))