http-fs 0.5.0

HTTP File Service library
Documentation
//!Actix adaptor
//!
//!## Usage
//!
//!```rust, no_run
//!use http_fs::config::{self, StaticFileConfig};
//!use http_fs::{StaticFiles};
//!
//!use std::path::Path;
//!
//!//Actix's service traits require Clone
//!#[derive(Clone)]
//!pub struct DirectoryConfig;
//!impl StaticFileConfig for DirectoryConfig {
//!    type FileService = config::DefaultConfig;
//!    type DirService = config::DefaultConfig;
//!
//!    fn handle_directory(&self, _path: &Path) -> bool {
//!        true
//!    }
//!}
//!
//!fn main() {
//!    let _ = actix_web::HttpServer::new(|| actix_web::App::new().service(StaticFiles::new(DirectoryConfig))).bind("127.0.0.1:8080").expect("To bind").run();
//!}
//!```

use crate::ServeEntry;
use crate::config::{StaticFileConfig};

use std::path::Path;

use futures::Stream;
use futures::future::FutureResult;
use actix_service::{NewService, Service};
use actix_web::dev::{PayloadStream, ResourceDef, HttpServiceFactory, ServiceConfig, ServiceRequest, ServiceResponse, Body};
use actix_web::middleware::encoding::BodyEncoding;
use actix_http::body::SizedStream;
use percent_encoding::{percent_decode};

use actix_web::{HttpResponse};
use actix_web::error::Error;

impl<C: 'static + StaticFileConfig + Clone> HttpServiceFactory<PayloadStream> for crate::StaticFiles<C> {
    fn register(self, config: &mut ServiceConfig<PayloadStream>) {
        let r_def = match config.is_root() {
            true => ResourceDef::root_prefix(self.config.router_prefix()),
            false => ResourceDef::prefix(self.config.router_prefix()),
        };

        config.register_service(r_def, None, self, None);
    }
}

impl<C: 'static + StaticFileConfig + Clone> NewService for crate::StaticFiles<C> {
    type Request = ServiceRequest;
    type Response = ServiceResponse;
    type Error = Error;
    type Service = Self;
    type InitError = ();
    type Future = FutureResult<Self::Service, Self::InitError>;

    fn new_service(&self, _: &()) -> Self::Future {
        futures::future::ok(self.clone())
    }
}

impl<C: 'static + StaticFileConfig + Clone> Service for crate::StaticFiles<C> {
    type Request = ServiceRequest;
    type Response = ServiceResponse;
    type Error = Error;
    type Future = FutureResult<Self::Response, Self::Error>;

    fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> {
        Ok(futures::Async::Ready(()))
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        let (req, _) = req.into_parts();

        if !C::is_method_allowed(req.method()) {
            let response = HttpResponse::MethodNotAllowed().header(http::header::CONTENT_TYPE, "text/plain")
                                                           .header(http::header::ALLOW, "GET, HEAD")
                                                           .body("This resource only supports GET and HEAD.");

            return futures::future::ok(ServiceResponse::new(req, response));
        }

        let path = req.match_info().get("tail").unwrap_or_else(|| "");
        let path = percent_decode(path.as_bytes()).decode_utf8().expect("To percent decode");
        let path = Path::new(path.trim_start_matches("/"));
        let result = self.serve(path);

        match result {
            ServeEntry::NotFound | ServeEntry::IoError(_) => {
                let mut response = HttpResponse::new(http::StatusCode::OK);
                let (code, body) = self.handle_not_found(&path, response.headers_mut());
                *response.status_mut() = code;

                if body.len() > 0 {
                    let body = actix_web::dev::ResponseBody::Body(Body::Bytes(body));
                    futures::future::ok(ServiceResponse::new(req, response.map_body(|_, _| body)))
                } else {
                    futures::future::ok(ServiceResponse::new(req, response))
                }
            },
            ServeEntry::Directory(path, dir) => {
                let response = HttpResponse::Ok().content_type("text/html; charset=utf-8").body(self.list_dir(&path, dir));
                futures::future::ok(ServiceResponse::new(req, response))
            },
            ServeEntry::File(file, meta, path) => {
                let mut response = HttpResponse::new(http::StatusCode::OK);
                let (code, body) = self.serve_file(&path, file, meta, req.method().clone(), req.headers(), response.headers_mut());
                *response.status_mut() = code;

                if response.headers().contains_key(http::header::CONTENT_RANGE) {
                    match code {
                        http::StatusCode::OK | http::StatusCode::PARTIAL_CONTENT => {
                            response.encoding(actix_web::http::ContentEncoding::Identity);
                        },
                        _ => ()
                    }
                }

                let res = match body {
                    Some(body) => {
                        let size = body.size;
                        let body = SizedStream::new(size as usize, body.map_err(|err| err.into()));
                        let body = actix_web::dev::ResponseBody::Body(Body::Message(Box::new(body)));
                        ServiceResponse::new(req, response.map_body(|_, _| body))
                    },
                    None => ServiceResponse::new(req, response),
                };

                futures::future::ok(res)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::{Async, Future};
    use actix_web::test::TestRequest;
    use std::time::{Duration, SystemTime};
    use crate::utils::display_to_header;

    #[derive(Clone)]
    pub struct DirectoryConfig;
    impl StaticFileConfig for DirectoryConfig {
        type FileService = crate::config::DefaultConfig;
        type DirService = crate::config::DefaultConfig;

        fn index_file(&self, _path: &Path) -> Option<&Path> {
            Some(Path::new("Cargo.toml"))
        }

        fn handle_directory(&self, _path: &Path) -> bool {
            true
        }

        fn thread_pool_builder() -> threadpool::ThreadPool {
            threadpool::Builder::new().num_threads(1).build()
        }
    }

    macro_rules! from_async_result {
        ($resp:expr) => {
            match $resp.poll().unwrap() {
                Async::Ready(res) => res,
                Async::NotReady => panic!("Future is not ready!?")
            }
        }
    }

    #[test]
    fn test_index_file() {
        let mut files = crate::StaticFiles::new(DirectoryConfig);

        let since = SystemTime::now() + Duration::from_secs(60);
        let since = httpdate::HttpDate::from(since);

        let req = TestRequest::with_uri("/").header(http::header::IF_MODIFIED_SINCE, display_to_header(&since)).to_srv_request();
        let mut resp = files.call(req);
        let resp = from_async_result!(resp);
        assert_eq!(resp.status(), http::StatusCode::NOT_MODIFIED);
    }

    #[test]
    fn test_if_modified_since_without_if_none_match() {
        let mut files = crate::StaticFiles::new(DirectoryConfig);

        let since = SystemTime::now() + Duration::from_secs(60);
        let since = httpdate::HttpDate::from(since);

        let req = TestRequest::with_uri("/Cargo.toml").header(http::header::IF_MODIFIED_SINCE, display_to_header(&since)).to_srv_request();
        let mut resp = files.call(req);
        let resp = from_async_result!(resp);
        assert_eq!(resp.status(), http::StatusCode::NOT_MODIFIED);
    }

    #[test]
    fn test_if_modified_since_with_if_none_match() {
        let mut files = crate::StaticFiles::new(DirectoryConfig);

        let since = SystemTime::now() + Duration::from_secs(60);
        let since = httpdate::HttpDate::from(since);

        let req = TestRequest::with_uri("/Cargo.toml").header(http::header::IF_MODIFIED_SINCE, display_to_header(&since))
                                                      .header(http::header::IF_NONE_MATCH, "wrong etag")
                                                      .to_srv_request();
        let mut resp = files.call(req);
        let resp = from_async_result!(resp);
        assert_ne!(resp.status(), http::StatusCode::NOT_MODIFIED);
    }

}