diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 95ec1c35f..4fe8fadb3 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [0.1.0-alpha.2] - 2019-04-xx + +* Add default handler support + + ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 8254c5fe5..60ccd81de 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -7,23 +7,23 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::{cmp, io}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_web::dev::{ + HttpServiceFactory, Payload, ResourceDef, ServiceConfig, ServiceFromRequest, + ServiceRequest, ServiceResponse, +}; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; +use actix_web::http::header::DispositionType; +use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use bytes::Bytes; +use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll, Stream}; use mime; use mime_guess::get_mime_type; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; -use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{ - HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, - ServiceResponse, -}; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::http::header::DispositionType; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; -use futures::future::{ok, FutureResult}; - mod error; mod named; mod range; @@ -32,6 +32,7 @@ use self::error::{FilesError, UriSegmentError}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; +type HttpService

= BoxedService, ServiceResponse, Error>; type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; @@ -232,8 +233,6 @@ pub struct Files { default: Rc>>>>, renderer: Rc, mime_override: Option>, - _chunk_size: usize, - _follow_symlinks: bool, file_flags: named::Flags, } @@ -245,8 +244,6 @@ impl Clone for Files { show_index: self.show_index, default: self.default.clone(), renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), @@ -274,8 +271,6 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, - _chunk_size: 0, - _follow_symlinks: false, file_flags: named::Flags::default(), } } @@ -334,6 +329,24 @@ impl Files { self.file_flags.set(named::Flags::LAST_MD, value); self } + + /// Sets default handler which is used when no matched file could be found. + pub fn default_handler(mut self, f: F) -> Self + where + F: IntoNewService, + U: NewService< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f.into_new_service().map_init_err(|_| ()), + ))))); + + self + } } impl

HttpServiceFactory

for Files

@@ -353,41 +366,95 @@ where } } -impl

NewService for Files

{ +impl NewService for Files

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = Error; - type Service = Self; + type Service = FilesService

; type InitError = (); - type Future = FutureResult; + type Future = Box>; fn new_service(&self, _: &()) -> Self::Future { - ok(self.clone()) + let mut srv = FilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: None, + renderer: self.renderer.clone(), + mime_override: self.mime_override.clone(), + file_flags: self.file_flags, + }; + + if let Some(ref default) = *self.default.borrow() { + Box::new( + default + .new_service(&()) + .map(move |default| { + srv.default = Some(default); + srv + }) + .map_err(|_| ()), + ) + } else { + Box::new(ok(srv)) + } } } -impl

Service for Files

{ +pub struct FilesService

{ + directory: PathBuf, + index: Option, + show_index: bool, + default: Option>, + renderer: Rc, + mime_override: Option>, + file_flags: named::Flags, +} + +impl

FilesService

{ + fn handle_err( + &mut self, + e: io::Error, + req: HttpRequest, + payload: Payload

, + ) -> Either< + FutureResult, + Box>, + > { + log::debug!("Files: Failed to handle {}: {}", req.path(), e); + if let Some(ref mut default) = self.default { + Either::B(default.call(ServiceRequest::from_parts(req, payload))) + } else { + Either::A(ok(ServiceResponse::from_err(e, req.clone()))) + } + } +} + +impl

Service for FilesService

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = Error; - type Future = FutureResult; + type Future = Either< + FutureResult, + Box>, + >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, req: ServiceRequest

) -> Self::Future { - let (req, _) = req.into_parts(); + let (req, pl) = req.into_parts(); let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, - Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return Either::A(ok(ServiceResponse::from_err(e, req.clone()))), }; // full filepath let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, - Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return self.handle_err(e, req, pl), }; if path.is_dir() { @@ -403,25 +470,25 @@ impl

Service for Files

{ } named_file.flags = self.file_flags; - match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), - } + Either::A(ok(match named_file.respond_to(&req) { + Ok(item) => ServiceResponse::new(req.clone(), item), + Err(e) => ServiceResponse::from_err(e, req.clone()), + })) } - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => return self.handle_err(e, req, pl), } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); let x = (self.renderer)(&dir, &req); match x { - Ok(resp) => ok(resp), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Ok(resp) => Either::A(ok(resp)), + Err(e) => return self.handle_err(e, req, pl), } } else { - ok(ServiceResponse::from_err( + Either::A(ok(ServiceResponse::from_err( FilesError::IsDirectory, req.clone(), - )) + ))) } } else { match NamedFile::open(path) { @@ -434,11 +501,15 @@ impl

Service for Files

{ named_file.flags = self.file_flags; match named_file.respond_to(&req) { - Ok(item) => ok(ServiceResponse::new(req.clone(), item)), - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Ok(item) => { + Either::A(ok(ServiceResponse::new(req.clone(), item))) + } + Err(e) => { + Either::A(ok(ServiceResponse::from_err(e, req.clone()))) + } } } - Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + Err(e) => self.handle_err(e, req, pl), } } } @@ -833,15 +904,15 @@ mod tests { let mut response = test::call_success(&mut srv, request); // with enabled compression - // { - // let te = response - // .headers() - // .get(header::TRANSFER_ENCODING) - // .unwrap() - // .to_str() - // .unwrap(); - // assert_eq!(te, "chunked"); - // } + { + let te = response + .headers() + .get(header::TRANSFER_ENCODING) + .unwrap() + .to_str() + .unwrap(); + assert_eq!(te, "chunked"); + } let bytes = test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { @@ -890,20 +961,32 @@ mod tests { assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } - // #[test] - // fn test_named_file_content_encoding() { - // let req = TestRequest::default().method(Method::GET).finish(); - // let file = NamedFile::open("Cargo.toml").unwrap(); + #[test] + fn test_named_file_content_encoding() { + let mut srv = test::init_service(App::new().enable_encoding().service( + web::resource("/").to(|| { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Identity) + }), + )); - // assert!(file.encoding.is_none()); - // let resp = file - // .set_content_encoding(ContentEncoding::Identity) - // .respond_to(&req) - // .unwrap(); + let request = TestRequest::get() + .uri("/") + .header(header::ACCEPT_ENCODING, "gzip") + .to_request(); + let res = test::call_success(&mut srv, request); + assert_eq!(res.status(), StatusCode::OK); - // assert!(resp.content_encoding().is_some()); - // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); - // } + assert_eq!( + res.headers() + .get(header::CONTENT_ENCODING) + .unwrap() + .to_str() + .unwrap(), + "identity" + ); + } #[test] fn test_named_file_allowed_method() { @@ -954,22 +1037,29 @@ mod tests { let _st: Files<()> = Files::new("/", "Cargo.toml"); } - // #[test] - // fn test_default_handler_file_missing() { - // let st = Files::new(".") - // .default_handler(|_: &_| "default content"); - // let req = TestRequest::with_uri("/missing") - // .param("tail", "missing") - // .finish(); + #[test] + fn test_default_handler_file_missing() { + let mut st = test::block_on( + Files::new("/", ".") + .default_handler(|req: ServiceRequest<_>| { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .new_service(&()), + ) + .unwrap(); + let req = TestRequest::with_uri("/missing").to_service(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.body(), - // &Body::Binary(Binary::Slice(b"default content")) - // ); - // } + let mut resp = test::call_success(&mut st, req); + assert_eq!(resp.status(), StatusCode::OK); + let bytes = + test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + + assert_eq!(bytes.freeze(), Bytes::from_static(b"default content")); + } // #[test] // fn test_serve_index() { diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 842a0e5e0..4ee1a3caa 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -43,7 +43,7 @@ pub struct NamedFile { pub(crate) content_disposition: header::ContentDisposition, pub(crate) md: Metadata, modified: Option, - encoding: Option, + pub(crate) encoding: Option, pub(crate) status_code: StatusCode, pub(crate) flags: Flags, } diff --git a/src/resource.rs b/src/resource.rs index b24e8dd51..957795cd7 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, + apply_transform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -254,7 +254,7 @@ where >, F: IntoTransform, { - let endpoint = ApplyTransform::new(mw, self.endpoint); + let endpoint = apply_transform(mw, self.endpoint); Resource { endpoint, rdef: self.rdef,