simi-cli 0.1.8

A command line tool to help build, test, serve a Simi app
Documentation
//! This is just a very simple and inefficient static files server.
// Goals:
//     * Serve `index.html` at `/`
//     * Serve [.wasm, .js, .html, .css] with correct mime type
//
// TODO: Yes, of course, make it:
//     * less messy
//     * more efficient???

use std::path::PathBuf;
use std::sync::Arc;

use futures::{future, Future};
use hyper::{
    self,
    service::{NewService, Service},
    Body, Error as HyperError, Method, Request, Response, Server, StatusCode,
};
use mime_guess;
use tokio_fs;
use tokio_io;

use crate::error::*;

type FutureResponse = Box<Future<Item = Response<Body>, Error = HyperError> + Send>;

static NOTFOUND: &[u8] = b"Not Found";

struct Config {
    root_folder: PathBuf,
}

pub struct StaticFiles {
    config: Arc<Config>,
}

impl Config {
    fn new<T>(path: T) -> Result<Self, Error>
    where
        T: Into<PathBuf>,
    {
        Ok(Self {
            root_folder: path.into().canonicalize()?,
        })
    }
}

impl NewService for StaticFiles {
    type ReqBody = Body;
    type ResBody = Body;
    type Error = HyperError;
    type InitError = HyperError;
    type Service = StaticFiles;
    type Future = Box<Future<Item = Self::Service, Error = Self::InitError> + Send>;
    fn new_service(&self) -> Self::Future {
        Box::new(future::ok(Self {
            config: self.config.clone(),
        }))
    }
}

impl Service for StaticFiles {
    type ReqBody = Body;
    type ResBody = Body;
    type Error = HyperError;
    type Future = FutureResponse;
    fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
        match (req.method(), req.uri().path()) {
            (&Method::GET, "/") => self.serve_file("/index.html"),
            (&Method::GET, path) => self.serve_file(path),
            _ => unreachable!(),
        }
    }
}

impl StaticFiles {
    pub fn new<T>(path: T) -> Result<Self, Error>
    where
        T: Into<PathBuf>,
    {
        Ok(Self {
            config: Arc::new(Config::new(path)?),
        })
    }

    pub fn start(self, port: u16) {
        let addr = format!("0.0.0.0:{}", port)
            .parse()
            .expect("Address not valid");
        let server = Server::bind(&addr)
            .serve(self)
            .map_err(|e| eprintln!("error: {}", e));

        println!("\nServing at {}", addr);
        hyper::rt::run(server);
    }

    pub fn serve_file(&mut self, path: &str) -> FutureResponse {
        let mut file_path = self.config.root_folder.clone();
        if path.starts_with("/") {
            file_path.push(&path[1..]);
        } else {
            file_path.push(path);
        }
        if file_path.extension().is_none() && !file_path.exists() {
            file_path.set_extension("js");
        }
        let ext = file_path
            .extension()
            .map(|s| s.to_str().unwrap_or(""))
            .unwrap_or("")
            .to_string();

        println!("Serving {}", path);
        Box::new(
            tokio_fs::file::File::open(file_path)
                .and_then(move |file| {
                    let buf: Vec<u8> = Vec::new();
                    tokio_io::io::read_to_end(file, buf)
                        .and_then(move |item| {
                            let mime_type =
                                mime_guess::get_mime_type_str(&ext).unwrap_or_else(|| {
                                    if ext == "wasm" {
                                        "application/wasm"
                                    } else {
                                        "*"
                                    }
                                });

                            let res = Response::builder()
                                .status(StatusCode::OK)
                                .header("Content-Type", mime_type)
                                .body(item.1.into())
                                .unwrap_or_else(|_| {
                                    Response::builder()
                                        .status(StatusCode::INTERNAL_SERVER_ERROR)
                                        .body(Body::empty())
                                        .unwrap()
                                });

                            Ok(res)
                        })
                        .or_else(|_| {
                            Ok(Response::builder()
                                .status(StatusCode::INTERNAL_SERVER_ERROR)
                                .body(Body::empty())
                                .unwrap())
                        })
                })
                .or_else(|_| {
                    Ok(Response::builder()
                        .status(StatusCode::NOT_FOUND)
                        .body(NOTFOUND.into())
                        .unwrap())
                }),
        )
    }
}