From 3b1618604ed4aadfaf4b4af46f23f151e5bc96ce Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 11 Apr 2020 11:57:19 +0600 Subject: [PATCH] migrate files --- ntex-files/Cargo.toml | 5 +- ntex-files/src/error.rs | 30 ++- ntex-files/src/lib.rs | 458 +++++++++++++++++++++++----------------- ntex-files/src/named.rs | 163 ++++++++------ 4 files changed, 385 insertions(+), 271 deletions(-) diff --git a/ntex-files/Cargo.toml b/ntex-files/Cargo.toml index de41c07a..fc8ab448 100644 --- a/ntex-files/Cargo.toml +++ b/ntex-files/Cargo.toml @@ -18,11 +18,12 @@ name = "ntex_files" path = "src/lib.rs" [dependencies] -ntex = { version = "0.1.7", default-features = false } +ntex = "0.1.7" bitflags = "1" bytes = "0.5.4" futures = "0.3.4" derive_more = "0.99.5" +hyperx = "1.0.0" log = "0.4" mime = "0.3" mime_guess = "2.0.1" @@ -30,4 +31,4 @@ percent-encoding = "2.1" v_htmlescape = "0.4" [dev-dependencies] -ntex = { version = "0.1.7", features=["openssl"] } +ntex = { version = "0.1.7", features=["openssl", "compress"] } diff --git a/ntex-files/src/error.rs b/ntex-files/src/error.rs index 9b30cbaa..e269eebc 100644 --- a/ntex-files/src/error.rs +++ b/ntex-files/src/error.rs @@ -1,23 +1,35 @@ -use actix_web::{http::StatusCode, HttpResponse, ResponseError}; -use derive_more::Display; +use derive_more::{Display, From}; +use ntex::http::StatusCode; +use ntex::web::error::{DefaultError, WebResponseError}; /// Errors which can occur when serving static files. -#[derive(Display, Debug, PartialEq)] +#[derive(Display, Debug, From)] pub enum FilesError { /// Path is not a directory #[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, /// Cannot render directory - #[display(fmt = "Unable to render directory without index file")] + #[display(fmt = "Unable to render directory without index file.")] 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` -impl ResponseError for FilesError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) +impl WebResponseError for FilesError { + fn status_code(&self) -> StatusCode { + match self { + FilesError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED, + _ => StatusCode::NOT_FOUND, + } } } @@ -35,7 +47,7 @@ pub enum UriSegmentError { } /// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { +impl WebResponseError for UriSegmentError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } diff --git a/ntex-files/src/lib.rs b/ntex-files/src/lib.rs index 78c0d9a2..86db9625 100644 --- a/ntex-files/src/lib.rs +++ b/ntex-files/src/lib.rs @@ -1,7 +1,10 @@ -#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)] +#![allow( + type_alias_bounds, + clippy::borrow_interior_mutable_const, + clippy::type_complexity +)] //! Static files support -use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File}; use std::future::Future; @@ -12,22 +15,21 @@ use std::rc::Rc; use std::task::{Context, Poll}; use std::{cmp, io}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use actix_web::dev::{ - AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest, - ServiceResponse, -}; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::guard::Guard; -use actix_web::http::header::{self, DispositionType}; -use actix_web::http::Method; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse}; use bytes::Bytes; use futures::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; use futures::Stream; +use hyperx::header::DispositionType; use mime; use mime_guess::from_ext; +use ntex::http::error::BlockingError; +use ntex::http::{header, Method, Payload, Uri}; +use ntex::router::{ResourceDef, ResourcePath}; +use ntex::service::boxed::{self, BoxService, BoxServiceFactory}; +use ntex::service::{IntoServiceFactory, Service, ServiceFactory}; +use ntex::web::dev::{WebRequest, WebResponse, WebServiceConfig, WebServiceFactory}; +use ntex::web::error::ErrorRenderer; +use ntex::web::guard::Guard; +use ntex::web::{self, FromRequest, HttpRequest, HttpResponse}; use percent_encoding::{utf8_percent_encode, CONTROLS}; use v_htmlescape::escape as escape_html_entity; @@ -39,8 +41,10 @@ use self::error::{FilesError, UriSegmentError}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; +type HttpService = + BoxService, WebResponse, Err::Container>; +type HttpServiceFactory = + BoxServiceFactory<(), WebRequest, WebResponse, Err::Container, ()>; /// 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 @@ -50,12 +54,6 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { from_ext(ext).first_or_octet_stream() } -fn handle_error(err: BlockingError) -> Error { - match err { - BlockingError::Error(err) => err.into(), - BlockingError::Canceled => ErrorInternalServerError("Unexpected error"), - } -} #[doc(hidden)] /// A helper created from a `std::fs::File` which reads the file /// chunk-by-chunk on a `ThreadPool`. @@ -69,7 +67,7 @@ pub struct ChunkedReadFile { } impl Stream for ChunkedReadFile { - type Item = Result; + type Item = Result; fn poll_next( mut self: Pin<&mut Self>, @@ -84,7 +82,15 @@ impl Stream for ChunkedReadFile { self.counter += bytes.len() as u64; Poll::Ready(Some(Ok(bytes))) } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))), + Poll::Ready(Err(e)) => { + let e = match e { + BlockingError::Error(e) => e, + BlockingError::Canceled => { + io::Error::new(io::ErrorKind::Other, "Operation is canceled") + } + }; + Poll::Ready(Some(Err(e))) + } Poll::Pending => Poll::Pending, }; } @@ -118,7 +124,7 @@ impl Stream for ChunkedReadFile { } type DirectoryRenderer = - dyn Fn(&Directory, &HttpRequest) -> Result; + dyn Fn(&Directory, &HttpRequest) -> Result; /// A directory; responds with the generated directory listing. #[derive(Debug)] @@ -169,7 +175,7 @@ macro_rules! encode_file_name { fn directory_listing( dir: &Directory, req: &HttpRequest, -) -> Result { +) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); @@ -217,7 +223,7 @@ fn directory_listing( \n", index_of, index_of, body ); - Ok(ServiceResponse::new( + Ok(WebResponse::new( req.clone(), HttpResponse::Ok() .content_type("text/html; charset=utf-8") @@ -232,28 +238,28 @@ type MimeOverride = dyn Fn(&mime::Name) -> DispositionType; /// `Files` service must be registered with `App::service()` method. /// /// ```rust -/// use actix_web::App; -/// use actix_files as fs; +/// use ntex::web::App; +/// use ntex_files as fs; /// /// fn main() { /// let app = App::new() /// .service(fs::Files::new("/static", ".")); /// } /// ``` -pub struct Files { +pub struct Files { path: String, directory: PathBuf, index: Option, show_index: bool, redirect_to_slash: bool, - default: Rc>>>, + default: Option>>, renderer: Rc, mime_override: Option>, file_flags: named::Flags, guards: Option>>, } -impl Clone for Files { +impl Clone for Files { fn clone(&self) -> Self { Self { directory: self.directory.clone(), @@ -270,13 +276,13 @@ impl Clone for Files { } } -impl Files { +impl Files { /// Create new `Files` instance for specified base directory. /// /// `File` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. - pub fn new>(path: &str, dir: T) -> Files { + pub fn new>(path: &str, dir: T) -> Self { let orig_dir = dir.into(); let dir = match orig_dir.canonicalize() { Ok(canon_dir) => canon_dir, @@ -292,7 +298,7 @@ impl Files { index: None, show_index: false, redirect_to_slash: false, - default: Rc::new(RefCell::new(None)), + default: None, renderer: Rc::new(directory_listing), mime_override: None, file_flags: named::Flags::default(), @@ -319,7 +325,7 @@ impl Files { /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + 'static, { self.renderer = Rc::new(f); @@ -386,24 +392,29 @@ impl Files { F: IntoServiceFactory, U: ServiceFactory< Config = (), - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, + Request = WebRequest, + Response = WebResponse, + Error = Err::Container, > + 'static, { // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( + self.default = Some(Rc::new(boxed::factory( f.into_factory().map_init_err(|_| ()), - ))))); + ))); self } } -impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { - if self.default.borrow().is_none() { - *self.default.borrow_mut() = Some(config.default_service()); +impl WebServiceFactory for Files +where + Err: ErrorRenderer, + Err::Container: From, + Err::Container: From, +{ + fn register(mut self, config: &mut WebServiceConfig) { + if self.default.is_none() { + self.default = Some(config.default_service()); } let rdef = if config.is_root() { ResourceDef::root_prefix(&self.path) @@ -414,12 +425,17 @@ impl HttpServiceFactory for Files { } } -impl ServiceFactory for Files { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; +impl ServiceFactory for Files +where + Err: ErrorRenderer, + Err::Container: From, + Err::Container: From, +{ + type Request = WebRequest; + type Response = WebResponse; + type Error = Err::Container; type Config = (); - type Service = FilesService; + type Service = FilesService; type InitError = (); type Future = LocalBoxFuture<'static, Result>; @@ -436,7 +452,7 @@ impl ServiceFactory for Files { guards: self.guards.clone(), }; - if let Some(ref default) = *self.default.borrow() { + if let Some(ref default) = self.default.as_ref() { default .new_service(()) .map(move |result| match result { @@ -453,50 +469,58 @@ impl ServiceFactory for Files { } } -pub struct FilesService { +pub struct FilesService { directory: PathBuf, index: Option, show_index: bool, redirect_to_slash: bool, - default: Option, + default: Option>, renderer: Rc, mime_override: Option>, file_flags: named::Flags, guards: Option>>, } -impl FilesService { - fn handle_err( - &mut self, +impl FilesService +where + Err::Container: From, +{ + fn handle_io_error( + &self, e: io::Error, - req: ServiceRequest, + req: WebRequest, ) -> Either< - Ready>, - LocalBoxFuture<'static, Result>, + Ready>, + LocalBoxFuture<'static, Result>, > { log::debug!("Files: Failed to handle {}: {}", req.path(), e); - if let Some(ref mut default) = self.default { + if let Some(ref default) = self.default { Either::Right(default.call(req)) } else { - Either::Left(ok(req.error_response(e))) + Either::Left(ok(req.error_response(FilesError::from(e)))) } } } -impl Service for FilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; +impl Service for FilesService +where + Err: ErrorRenderer, + Err::Container: From, + Err::Container: From, +{ + type Request = WebRequest; + type Response = WebResponse; + type Error = Err::Container; type Future = Either< Ready>, LocalBoxFuture<'static, Result>, >; - fn poll_ready(&mut self, _: &mut Context) -> Poll> { + fn poll_ready(&self, _: &mut Context) -> Poll> { Poll::Ready(Ok(())) } - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: WebRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards (**guard).check(req.head()) @@ -509,24 +533,34 @@ impl Service for FilesService { }; if !is_method_valid { - return Either::Left(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .body("Request did not meet this resource's requirements."), - ))); + return Either::Left(ok(req.error_response(FilesError::MethodNotAllowed))); } + println!("R: {:?}", req.match_info().path()); + let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, Err(e) => return Either::Left(ok(req.error_response(e))), }; + println!( + "R: {:?} {:?} {:?}", + real_path, + std::env::current_dir(), + self.directory.join(&real_path.0) + ); + // full filepath let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, - Err(e) => return self.handle_err(e, req), + Err(e) => { + println!("TEST: {:?}", e); + return self.handle_io_error(e, req); + } }; + println!("R2: {:?}", path); + if path.is_dir() { if let Some(ref redir_index) = self.index { if self.redirect_to_slash && !req.path().ends_with('/') { @@ -552,11 +586,11 @@ impl Service for FilesService { named_file.flags = self.file_flags; let (req, _) = req.into_parts(); Either::Left(ok(match named_file.into_response(&req) { - Ok(item) => ServiceResponse::new(req, item), - Err(e) => ServiceResponse::from_err(e, req), + Ok(item) => WebResponse::new(req, item), + Err(_) => unreachable!(), })) } - Err(e) => self.handle_err(e, req), + Err(e) => self.handle_io_error(e, req), } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); @@ -564,10 +598,13 @@ impl Service for FilesService { let x = (self.renderer)(&dir, &req); match x { Ok(resp) => Either::Left(ok(resp)), - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + Err(e) => Either::Left(ok(WebResponse::from_err::( + FilesError::from(e), + req, + ))), } } else { - Either::Left(ok(ServiceResponse::from_err( + Either::Left(ok(WebResponse::from_err::( FilesError::IsDirectory, req.into_parts().0, ))) @@ -585,12 +622,12 @@ impl Service for FilesService { let (req, _) = req.into_parts(); match named_file.into_response(&req) { Ok(item) => { - Either::Left(ok(ServiceResponse::new(req.clone(), item))) + Either::Left(ok(WebResponse::new(req.clone(), item))) } - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + Err(_) => unreachable!(), } } - Err(e) => self.handle_err(e, req), + Err(e) => self.handle_io_error(e, req), } } } @@ -620,7 +657,7 @@ impl PathBufWrp { } else if cfg!(windows) && segment.contains('\\') { return Err(UriSegmentError::BadChar('\\')); } else { - buf.push(segment) + buf.push(Uri::unquote(segment).as_ref()) } } @@ -628,10 +665,9 @@ impl PathBufWrp { } } -impl FromRequest for PathBufWrp { +impl FromRequest for PathBufWrp { type Error = UriSegmentError; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ready(PathBufWrp::get_pathbuf(req.match_info().path())) @@ -646,16 +682,12 @@ mod tests { use std::time::{Duration, SystemTime}; use super::*; - use actix_web::guard; - use actix_web::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, - }; - use actix_web::http::{Method, StatusCode}; - use actix_web::middleware::Compress; - use actix_web::test::{self, TestRequest}; - use actix_web::{App, Responder}; + use ntex::http::{self, Method, StatusCode}; + use ntex::web::middleware::Compress; + use ntex::web::test::{self, TestRequest}; + use ntex::web::{guard, App, DefaultError}; - #[actix_rt::test] + #[ntex::test] async fn test_file_extension_to_mime() { let m = file_extension_to_mime("jpg"); assert_eq!(m, mime::IMAGE_JPEG); @@ -667,34 +699,36 @@ mod tests { assert_eq!(m, mime::APPLICATION_OCTET_STREAM); } - #[actix_rt::test] + #[ntex::test] async fn test_if_modified_since_without_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let since = hyperx::header::HttpDate::from( + SystemTime::now().add(Duration::from_secs(60)), + ); let req = TestRequest::default() - .header(header::IF_MODIFIED_SINCE, since) + .header(http::header::IF_MODIFIED_SINCE, since.to_string()) .to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); } - #[actix_rt::test] + #[ntex::test] async fn test_if_modified_since_with_if_none_match() { let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let since = hyperx::header::HttpDate::from( + SystemTime::now().add(Duration::from_secs(60)), + ); let req = TestRequest::default() - .header(header::IF_NONE_MATCH, "miss_etag") - .header(header::IF_MODIFIED_SINCE, since) + .header(http::header::IF_NONE_MATCH, "miss_etag") + .header(http::header::IF_MODIFIED_SINCE, since.to_string()) .to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_text() { assert!(NamedFile::open("test--").is_err()); let mut file = NamedFile::open("Cargo.toml").unwrap(); @@ -707,18 +741,20 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "inline; filename=\"Cargo.toml\"" ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_content_disposition() { assert!(NamedFile::open("test--").is_err()); let mut file = NamedFile::open("Cargo.toml").unwrap(); @@ -731,9 +767,11 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "inline; filename=\"Cargo.toml\"" ); @@ -741,36 +779,41 @@ mod tests { .unwrap() .disable_content_disposition(); let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); - assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none()); + let resp = test::respond_to(file, &req).await.unwrap(); + assert!(resp + .headers() + .get(http::header::CONTENT_DISPOSITION) + .is_none()); } - #[actix_rt::test] - async fn test_named_file_non_ascii_file_name() { - let mut file = - NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") - .unwrap(); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } + // #[ntex::test] + // async fn test_named_file_non_ascii_file_name() { + // let mut file = + // NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") + // .unwrap(); + // { + // file.file(); + // let _f: &File = &file; + // } + // { + // let _f: &mut File = &mut file; + // } - let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml" - ); - } + // let req = TestRequest::default().to_http_request(); + // let resp = test::respond_to(file, &req).await.unwrap(); + // assert_eq!( + // resp.headers().get(http::header::CONTENT_TYPE).unwrap(), + // "text/x-toml" + // ); + // assert_eq!( + // resp.headers() + // .get(http::header::CONTENT_DISPOSITION) + // .unwrap(), + // "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml" + // ); + // } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_set_content_type() { let mut file = NamedFile::open("Cargo.toml") .unwrap() @@ -784,18 +827,20 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/xml" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "inline; filename=\"Cargo.toml\"" ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_image() { let mut file = NamedFile::open("tests/test.png").unwrap(); { @@ -807,22 +852,32 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "inline; filename=\"test.png\"" ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_image_attachment() { + use hyperx::header::{ + Charset, ContentDisposition, DispositionParam, DispositionType, + }; + let cd = ContentDisposition { disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename(String::from("test.png"))], + parameters: vec![DispositionParam::Filename( + Charset::Ext(String::from("UTF-8")), + None, + "test.png".to_string().into_bytes(), + )], }; let mut file = NamedFile::open("tests/test.png") .unwrap() @@ -836,18 +891,20 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "attachment; filename=\"test.png\"" ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_binary() { let mut file = NamedFile::open("tests/test.binary").unwrap(); { @@ -859,18 +916,20 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "application/octet-stream" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "attachment; filename=\"test.binary\"" ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_status_code_text() { let mut file = NamedFile::open("Cargo.toml") .unwrap() @@ -884,19 +943,21 @@ mod tests { } let req = TestRequest::default().to_http_request(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml" ); assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + resp.headers() + .get(http::header::CONTENT_DISPOSITION) + .unwrap(), "inline; filename=\"Cargo.toml\"" ); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } - #[actix_rt::test] + #[ntex::test] async fn test_mime_override() { fn all_attachment(_: &mime::Name) -> DispositionType { DispositionType::Attachment @@ -917,7 +978,7 @@ mod tests { let content_disposition = response .headers() - .get(header::CONTENT_DISPOSITION) + .get(http::header::CONTENT_DISPOSITION) .expect("To have CONTENT_DISPOSITION"); let content_disposition = content_disposition .to_str() @@ -925,7 +986,7 @@ mod tests { assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\""); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_ranges_status_code() { let mut srv = test::init_service( App::new().service(Files::new("/test", ".").index_file("Cargo.toml")), @@ -935,7 +996,7 @@ mod tests { // Valid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") - .header(header::RANGE, "bytes=10-20") + .header(http::header::RANGE, "bytes=10-20") .to_request(); let response = test::call_service(&mut srv, request).await; assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); @@ -943,14 +1004,14 @@ mod tests { // Invalid range header let request = TestRequest::get() .uri("/t%65st/Cargo.toml") - .header(header::RANGE, "bytes=1-0") + .header(http::header::RANGE, "bytes=1-0") .to_request(); let response = test::call_service(&mut srv, request).await; assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_content_range_headers() { let mut srv = test::init_service( App::new().service(Files::new("/test", ".").index_file("tests/test.binary")), @@ -960,13 +1021,13 @@ mod tests { // Valid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") - .header(header::RANGE, "bytes=10-20") + .header(http::header::RANGE, "bytes=10-20") .to_request(); let response = test::call_service(&mut srv, request).await; let contentrange = response .headers() - .get(header::CONTENT_RANGE) + .get(http::header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(); @@ -976,13 +1037,13 @@ mod tests { // Invalid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") - .header(header::RANGE, "bytes=10-5") + .header(http::header::RANGE, "bytes=10-5") .to_request(); let response = test::call_service(&mut srv, request).await; let contentrange = response .headers() - .get(header::CONTENT_RANGE) + .get(http::header::CONTENT_RANGE) .unwrap() .to_str() .unwrap(); @@ -990,7 +1051,7 @@ mod tests { assert_eq!(contentrange, "bytes */100"); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_content_length_headers() { // use actix_web::body::{MessageBody, ResponseBody}; @@ -1002,7 +1063,7 @@ mod tests { // Valid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") - .header(header::RANGE, "bytes=10-20") + .header(http::header::RANGE, "bytes=10-20") .to_request(); let _response = test::call_service(&mut srv, request).await; @@ -1017,7 +1078,7 @@ mod tests { // Invalid range header let request = TestRequest::get() .uri("/t%65st/tests/test.binary") - .header(header::RANGE, "bytes=10-8") + .header(http::header::RANGE, "bytes=10-8") .to_request(); let response = test::call_service(&mut srv, request).await; assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); @@ -1059,7 +1120,7 @@ mod tests { assert_eq!(bytes, data); } - #[actix_rt::test] + #[ntex::test] async fn test_head_content_length_headers() { let mut srv = test::init_service( App::new().service(Files::new("test", ".").index_file("tests/test.binary")), @@ -1083,16 +1144,17 @@ mod tests { // assert_eq!(contentlength, "100"); } - #[actix_rt::test] + #[ntex::test] async fn test_static_files_with_spaces() { - let mut srv = test::init_service( + let srv = test::init_service( App::new().service(Files::new("/", ".").index_file("Cargo.toml")), ) .await; let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); - let response = test::call_service(&mut srv, request).await; + let response = test::call_service(&srv, request).await; + println!("TEST- {:?}", response); assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; @@ -1100,7 +1162,7 @@ mod tests { assert_eq!(bytes, data); } - #[actix_rt::test] + #[ntex::test] async fn test_files_not_allowed() { let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await; @@ -1121,7 +1183,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } - #[actix_rt::test] + #[ntex::test] async fn test_files_guards() { let mut srv = test::init_service( App::new().service(Files::new("/", ".").use_guards(guard::Post())), @@ -1137,46 +1199,46 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_content_encoding() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { NamedFile::open("Cargo.toml") .unwrap() - .set_content_encoding(header::ContentEncoding::Identity) + .set_content_encoding(http::header::ContentEncoding::Identity) }), )) .await; let request = TestRequest::get() .uri("/") - .header(header::ACCEPT_ENCODING, "gzip") + .header(http::header::ACCEPT_ENCODING, "gzip") .to_request(); let res = test::call_service(&mut srv, request).await; assert_eq!(res.status(), StatusCode::OK); - assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + assert!(!res.headers().contains_key(http::header::CONTENT_ENCODING)); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_content_encoding_gzip() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { NamedFile::open("Cargo.toml") .unwrap() - .set_content_encoding(header::ContentEncoding::Gzip) + .set_content_encoding(http::header::ContentEncoding::Gzip) }), )) .await; let request = TestRequest::get() .uri("/") - .header(header::ACCEPT_ENCODING, "gzip") + .header(http::header::ACCEPT_ENCODING, "gzip") .to_request(); let res = test::call_service(&mut srv, request).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers() - .get(header::CONTENT_ENCODING) + .get(http::header::CONTENT_ENCODING) .unwrap() .to_str() .unwrap(), @@ -1184,15 +1246,15 @@ mod tests { ); } - #[actix_rt::test] + #[ntex::test] async fn test_named_file_allowed_method() { let req = TestRequest::default().method(Method::GET).to_http_request(); let file = NamedFile::open("Cargo.toml").unwrap(); - let resp = file.respond_to(&req).await.unwrap(); + let resp = test::respond_to(file, &req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } - #[actix_rt::test] + #[ntex::test] async fn test_static_files() { let mut srv = test::init_service( App::new().service(Files::new("/", ".").show_files_listing()), @@ -1216,7 +1278,7 @@ mod tests { let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&mut srv, req).await; assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" ); @@ -1224,7 +1286,7 @@ mod tests { assert!(format!("{:?}", bytes).contains("/tests/test.png")); } - #[actix_rt::test] + #[ntex::test] async fn test_redirect_to_slash_directory() { // should not redirect if no index let mut srv = test::init_service( @@ -1254,16 +1316,16 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } - #[actix_rt::test] + #[ntex::test] async fn test_static_files_bad_directory() { - let _st: Files = Files::new("/", "missing"); - let _st: Files = Files::new("/", "Cargo.toml"); + let _st: Files = Files::new("/", "missing"); + let _st: Files = Files::new("/", "Cargo.toml"); } - #[actix_rt::test] + #[ntex::test] async fn test_default_handler_file_missing() { let mut st = Files::new("/", ".") - .default_handler(|req: ServiceRequest| { + .default_handler(|req: WebRequest| { ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .new_service(()) @@ -1277,7 +1339,7 @@ mod tests { assert_eq!(bytes, Bytes::from_static(b"default content")); } - // #[actix_rt::test] + // #[ntex::test] // async fn test_serve_index() { // let st = Files::new(".").index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); @@ -1323,7 +1385,7 @@ mod tests { // assert_eq!(resp.status(), StatusCode::NOT_FOUND); // } - // #[actix_rt::test] + // #[ntex::test] // async fn test_serve_index_nested() { // let st = Files::new(".").index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); @@ -1340,7 +1402,7 @@ mod tests { // ); // } - // #[actix_rt::test] + // #[ntex::test] // fn integration_serve_index() { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( @@ -1373,7 +1435,7 @@ mod tests { // assert_eq!(response.status(), StatusCode::NOT_FOUND); // } - // #[actix_rt::test] + // #[ntex::test] // fn integration_percent_encoded() { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( @@ -1391,7 +1453,7 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } - #[actix_rt::test] + #[ntex::test] async fn test_path_buf() { assert_eq!( PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), diff --git a/ntex-files/src/named.rs b/ntex-files/src/named.rs index fdb05599..a56e4ccf 100644 --- a/ntex-files/src/named.rs +++ b/ntex-files/src/named.rs @@ -1,3 +1,5 @@ +use std::convert::Infallible; +use std::error::Error; use std::fs::{File, Metadata}; use std::io; use std::ops::{Deref, DerefMut}; @@ -11,14 +13,16 @@ use bitflags::bitflags; use mime; 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::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::ChunkedReadFile; @@ -60,7 +64,7 @@ impl NamedFile { /// # Examples /// /// ```rust - /// use actix_files::NamedFile; + /// use ntex_files::NamedFile; /// use std::io::{self, Write}; /// use std::env; /// use std::fs::File; @@ -94,15 +98,11 @@ impl NamedFile { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; - let mut parameters = - vec![DispositionParam::Filename(String::from(filename.as_ref()))]; - if !filename.is_ascii() { - parameters.push(DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: filename.into_owned().into_bytes(), - })) - } + let parameters = vec![DispositionParam::Filename( + Charset::Ext(String::from("UTF-8")), + None, + filename.into_owned().into_bytes(), + )]; let cd = ContentDisposition { disposition: disposition_type, parameters: parameters, @@ -131,7 +131,7 @@ impl NamedFile { /// # Examples /// /// ```rust - /// use actix_files::NamedFile; + /// use ntex_files::NamedFile; /// /// let file = NamedFile::open("foo.txt"); /// ``` @@ -150,10 +150,9 @@ impl NamedFile { /// # Examples /// /// ```rust - /// # use std::io; - /// use actix_files::NamedFile; + /// use ntex_files::NamedFile; /// - /// # fn path() -> io::Result<()> { + /// # fn path() -> std::io::Result<()> { /// let file = NamedFile::open("test.txt")?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); /// # Ok(()) @@ -257,13 +256,13 @@ impl NamedFile { self.modified.map(|mtime| mtime.into()) } - pub fn into_response(self, req: &HttpRequest) -> Result { + pub fn into_response(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(http::header::CONTENT_TYPE, self.content_type.to_string()) .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { res.header( - header::CONTENT_DISPOSITION, + http::header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); }); @@ -294,9 +293,19 @@ impl NamedFile { // check preconditions let precondition_failed = if !any_match(etag.as_ref(), req) { true - } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = - (last_modified, req.get_header()) - { + } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = { + 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 t2: SystemTime = since.clone().into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { @@ -310,11 +319,20 @@ impl NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true - } else if req.headers().contains_key(&header::IF_NONE_MATCH) { + } else if req.headers().contains_key(&http::header::IF_NONE_MATCH) { false - } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = - (last_modified, req.get_header()) - { + } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = { + 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 t2: SystemTime = since.clone().into(); 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); - 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| { res.header( - header::CONTENT_DISPOSITION, + http::header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); }); @@ -339,26 +357,29 @@ impl NamedFile { } 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| { - 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 offset = 0; // 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(rangesvec) = HttpRange::parse(rangesheader, length) { length = rangesvec[0].length; offset = rangesvec[0].start; resp.encoding(ContentEncoding::Identity); resp.header( - header::CONTENT_RANGE, + http::header::CONTENT_RANGE, format!( "bytes {}-{}/{}", offset, @@ -367,7 +388,10 @@ impl NamedFile { ), ); } 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()); }; } else { @@ -391,7 +415,13 @@ impl NamedFile { if offset != 0 || length != self.md.len() { Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)) } else { - Ok(resp.body(SizedStream::new(length, reader))) + Ok(resp.body(SizedStream::new( + length, + reader.map_err(|e| { + let e: Box = 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`. fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; + if let Some(val) = req.headers().get(http::header::IF_MATCH) { + if let Ok(val) = header::IfMatch::parse_header(&val) { + match val { + header::IfMatch::Any => return true, + header::IfMatch::Items(ref items) => { + if let Some(some_etag) = etag { + 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`. fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; + if let Some(val) = req.headers().get(http::header::IF_NONE_MATCH) { + if let Ok(val) = header::IfNoneMatch::parse_header(&val) { + return match val { + header::IfNoneMatch::Any => false, + header::IfNoneMatch::Items(ref items) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } } + true } - } - true + }; } - None => true, } + true } -impl Responder for NamedFile { - type Error = Error; - type Future = Ready>; +impl Responder for NamedFile { + type Error = Infallible; + type Future = Ready>; fn respond_to(self, req: &HttpRequest) -> Self::Future { ready(self.into_response(req))