From ba1a73443e027c552ad3f6eb8b16595b04162d6e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 16 Oct 2017 01:19:23 -0700 Subject: [PATCH] added StaticFiles basic impl --- Cargo.toml | 1 + src/httprequest.rs | 2 +- src/httpresponse.rs | 2 +- src/lib.rs | 7 +- src/main.rs | 1 + src/route.rs | 8 +- src/server.rs | 2 +- src/staticfiles.rs | 177 ++++++++++++++++++++++++++++++++++++++++++-- 8 files changed, 186 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92cdf180..ca6fdbfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ time = "0.1" http = "0.1" httparse = "0.1" http-range = "0.1" +mime_guess = "1.8" cookie = { version="0.10", features=["percent-encode"] } regex = "0.2" slab = "0.4" diff --git a/src/httprequest.rs b/src/httprequest.rs index 5e8fe9bf..6ab0e9cb 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -80,7 +80,7 @@ impl HttpRequest { pub fn cookie(&self, name: &str) -> Option<&Cookie> { for cookie in &self.cookies { if cookie.name() == name { - return Some(&cookie) + return Some(cookie) } } None diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 9cf3e628..bc735827 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -117,7 +117,7 @@ impl HttpResponse { /// Get custom reason for the response. #[inline] pub fn reason(&self) -> &str { - if let Some(ref reason) = self.reason { + if let Some(reason) = self.reason { reason } else { "" diff --git a/src/lib.rs b/src/lib.rs index d57e196c..ec15e54c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,9 @@ extern crate log; extern crate time; extern crate bytes; extern crate sha1; -extern crate regex; -#[macro_use] -extern crate lazy_static; +// extern crate regex; +// #[macro_use] +// extern crate lazy_static; #[macro_use] extern crate futures; extern crate tokio_core; @@ -22,6 +22,7 @@ extern crate cookie; extern crate http; extern crate httparse; extern crate http_range; +extern crate mime_guess; extern crate route_recognizer; extern crate url; extern crate actix; diff --git a/src/main.rs b/src/main.rs index 390c92cd..a55be82f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,6 +109,7 @@ fn main() { r.get::(); r.post::(); }) + .route_handler("/static", StaticFiles::new(".", true)) .finish()) .resource("/test", |r| r.post::()) .resource("/ws/", |r| r.get::()) diff --git a/src/route.rs b/src/route.rs index c2edeff9..48793160 100644 --- a/src/route.rs +++ b/src/route.rs @@ -24,12 +24,13 @@ pub enum Frame { } /// Trait defines object that could be regestered as resource route +#[allow(unused_variables)] pub trait RouteHandler: 'static { /// Handle request fn handle(&self, req: HttpRequest, payload: Payload, state: Rc) -> Task; /// Set route prefix - fn set_prefix(&mut self, _prefix: String) {} + fn set_prefix(&mut self, prefix: String) {} } /// Actors with ability to handle http requests. @@ -39,7 +40,7 @@ pub trait Route: Actor { /// and could be accessed with `HttpContext::state()` method. type State; - /// Handle `EXPECT` header. By default respond with `HTTP/1.1 100 Continue` + /// Handle `EXPECT` header. By default respones with `HTTP/1.1 100 Continue` fn expect(req: &HttpRequest, ctx: &mut Self::Context) -> Result<(), HttpResponse> where Self: Actor> { @@ -68,7 +69,8 @@ pub trait Route: Actor { /// Handle incoming request. Route actor can return /// result immediately with `Reply::reply`. - /// Actor itself could be returned for handling streaming request/response. + /// Actor itself can be returned with `Reply::stream` for handling streaming + /// request/response or websocket connection. /// In that case `HttpContext::start` and `HttpContext::write` has to be used /// for writing response. fn request(req: HttpRequest, payload: Payload, ctx: &mut Self::Context) -> Reply; diff --git a/src/server.rs b/src/server.rs index 9b050881..ac78de02 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,7 +34,7 @@ impl HttpServer { { let mut err = None; let mut addrs = Vec::new(); - for iter in addr.to_socket_addrs() { + if let Ok(iter) = addr.to_socket_addrs() { for addr in iter { match TcpListener::bind(&addr, Arbiter::handle()) { Ok(tcp) => addrs.push(tcp), diff --git a/src/staticfiles.rs b/src/staticfiles.rs index ff60ff8d..d73c964f 100644 --- a/src/staticfiles.rs +++ b/src/staticfiles.rs @@ -1,23 +1,190 @@ #![allow(dead_code, unused_variables)] +use std::io; +use std::io::Read; use std::rc::Rc; +use std::fmt::Write; +use std::fs::{File, DirEntry}; +use std::path::PathBuf; use task::Task; use route::RouteHandler; use payload::Payload; -use httpcodes::HTTPOk; +use mime_guess::get_mime_type; use httprequest::HttpRequest; +use httpresponse::{Body, HttpResponse}; +use httpcodes::{HTTPOk, HTTPNotFound, HTTPForbidden, HTTPInternalServerError}; - +/// Static files handling +/// +/// Can be registered with `Application::route_handler()`. +/// +/// ```rust +/// extern crate actix_web; +/// use actix_web::*; +/// +/// fn main() { +/// let app = Application::default() +/// .route_handler("/static", StaticFiles::new(".", true)) +/// .finish(); +/// } +/// ``` pub struct StaticFiles { - directory: String, + directory: PathBuf, + accessible: bool, show_index: bool, chunk_size: usize, - follow_synlinks: bool, + follow_symlinks: bool, + prefix: String, +} + +impl StaticFiles { + /// Create new `StaticFiles` instance + /// + /// `dir` - base directory + /// `index` - show index for directory + pub fn new>(dir: D, index: bool) -> StaticFiles { + let dir = dir.into(); + + let (dir, access) = if let Ok(dir) = dir.canonicalize() { + if dir.is_dir() { + (dir, true) + } else { + (dir, false) + } + } else { + (dir, false) + }; + + StaticFiles { + directory: dir, + accessible: access, + show_index: index, + chunk_size: 0, + follow_symlinks: false, + prefix: String::new(), + } + } + + fn index(&self, relpath: &str, filename: PathBuf) -> Result { + let index_of = format!("Index of {}/{}", self.prefix, relpath); + let mut body = String::new(); + + for entry in filename.read_dir()? { + if self.can_list(&entry) { + let entry = entry.unwrap(); + // show file url as relative to static path + let file_url = format!( + "{}{}", self.prefix, + entry.path().strip_prefix(&self.directory).unwrap().to_string_lossy()); + + // if file is a directory, add '/' to the end of the name + let file_name = if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + //format!("
  • {}
  • ", file_url, file_name)); + write!(body, "
  • {}/
  • ", + file_url, entry.file_name().to_string_lossy()) + } else { + // write!(body, "{}/", entry.file_name()) + write!(body, "
  • {}
  • ", + file_url, entry.file_name().to_string_lossy()) + } + } else { + continue + }; + } + } + + let html = format!("\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", index_of, index_of, body); + Ok( + HTTPOk.builder() + .content_type("text/html; charset=utf-8") + .body(Body::Binary(html.into())).unwrap() + ) + } + + fn can_list(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink() + } + } + false + } } impl RouteHandler for StaticFiles { + fn set_prefix(&mut self, prefix: String) { + self.prefix += &prefix; + } + fn handle(&self, req: HttpRequest, payload: Payload, state: Rc) -> Task { - Task::reply(HTTPOk) + if !self.accessible { + Task::reply(HTTPNotFound) + } else { + let mut hidden = false; + let filepath = req.path()[self.prefix.len()..] + .split('/').filter(|s| { + if s.starts_with('.') { + hidden = true; + } + !s.is_empty() + }) + .fold(String::new(), |s, i| {s + "/" + i}); + + // hidden file + if hidden { + return Task::reply(HTTPNotFound) + } + + // full filepath + let idx = if filepath.starts_with('/') { 1 } else { 0 }; + let filename = match self.directory.join(&filepath[idx..]).canonicalize() { + Ok(fname) => fname, + Err(err) => return match err.kind() { + io::ErrorKind::NotFound => Task::reply(HTTPNotFound), + io::ErrorKind::PermissionDenied => Task::reply(HTTPForbidden), + _ => Task::reply(HTTPInternalServerError), + } + }; + + if filename.is_dir() { + match self.index(&filepath[idx..], filename) { + Ok(resp) => Task::reply(resp), + Err(err) => match err.kind() { + io::ErrorKind::NotFound => Task::reply(HTTPNotFound), + io::ErrorKind::PermissionDenied => Task::reply(HTTPForbidden), + _ => Task::reply(HTTPInternalServerError), + } + } + } else { + let mut resp = HTTPOk.builder(); + if let Some(ext) = filename.extension() { + let mime = get_mime_type(&ext.to_string_lossy()); + resp.content_type(format!("{}", mime).as_str()); + } + match File::open(filename) { + Ok(mut file) => { + let mut data = Vec::new(); + let _ = file.read_to_end(&mut data); + Task::reply(resp.body(Body::Binary(data.into())).unwrap()) + }, + Err(err) => { + Task::reply(HTTPInternalServerError) + } + } + } + } } }