use std::path::{PathBuf, Path};
use std::fs;
use std::error::Error;
use std::fmt;
#[cfg(feature = "cache")]
use time::{self, Timespec};
#[cfg(feature = "cache")]
use std::time::Duration;
use iron::prelude::*;
use iron::{Handler, Url, status};
#[cfg(feature = "cache")]
use iron::modifier::Modifier;
use iron::modifiers::Redirect;
use mount::OriginalUrl;
use requested_path::RequestedPath;
use url;
#[derive(Clone)]
pub struct Static {
pub root: PathBuf,
#[cfg(feature = "cache")]
cache: Option<Cache>,
}
impl Static {
#[cfg(feature = "cache")]
pub fn new<P: Into<PathBuf>>(root: P) -> Static {
Static {
root: root.into(),
cache: None
}
}
#[cfg(not(feature = "cache"))]
pub fn new<P: Into<PathBuf>>(root: P) -> Static {
Static {
root: root.into(),
}
}
#[cfg(feature = "cache")]
pub fn cache(self, duration: Duration) -> Static {
self.set(Cache::new(duration))
}
#[cfg(feature = "cache")]
fn try_cache<P: AsRef<Path>>(&self, req: &mut Request, path: P) -> IronResult<Response> {
match self.cache {
None => Ok(Response::with((status::Ok, path.as_ref()))),
Some(ref cache) => cache.handle(req, path.as_ref()),
}
}
}
impl Handler for Static {
fn handle(&self, req: &mut Request) -> IronResult<Response> {
use std::io;
let requested_path = RequestedPath::new(&self.root, req);
let metadata = match fs::metadata(&requested_path.path) {
Ok(meta) => meta,
Err(e) => {
let status = match e.kind() {
io::ErrorKind::NotFound => status::NotFound,
io::ErrorKind::PermissionDenied => status::Forbidden,
_ => status::InternalServerError,
};
return Err(IronError::new(e, status))
},
};
if requested_path.should_redirect(&metadata, req) {
let mut original_url: url::Url = match req.extensions.get::<OriginalUrl>() {
None => &req.url,
Some(original_url) => original_url,
}.clone().into();
original_url.path_segments_mut().unwrap().push("");
let redirect_path = Url::from_generic_url(original_url).unwrap();
return Ok(Response::with((status::MovedPermanently,
format!("Redirecting to {}", redirect_path),
Redirect(redirect_path))));
}
match requested_path.get_file(&metadata) {
None => Err(IronError::new(NoFile, status::NotFound)),
#[cfg(feature = "cache")]
Some(path) => self.try_cache(req, path),
#[cfg(not(feature = "cache"))]
Some(path) => {
let path: &Path = &path;
Ok(Response::with((status::Ok, path)))
},
}
}
}
impl Set for Static {}
#[cfg(feature = "cache")]
#[derive(Clone)]
pub struct Cache {
pub duration: Duration,
}
#[cfg(feature = "cache")]
impl Cache {
pub fn new(duration: Duration) -> Cache {
Cache { duration: duration }
}
fn handle<P: AsRef<Path>>(&self, req: &mut Request, path: P) -> IronResult<Response> {
use iron::headers::{IfModifiedSince, HttpDate};
let path = path.as_ref();
let (size, last_modified_time) = match fs::metadata(path) {
Err(error) => return Err(IronError::new(error, status::InternalServerError)),
Ok(metadata) => {
use filetime::FileTime;
let time = FileTime::from_last_modification_time(&metadata);
(metadata.len(), Timespec::new(time.seconds() as i64, 0))
},
};
let if_modified_since = match req.headers.get::<IfModifiedSince>().cloned() {
None => return self.response_with_cache(req, path, size, last_modified_time),
Some(IfModifiedSince(HttpDate(time))) => time.to_timespec(),
};
if last_modified_time <= if_modified_since {
Ok(Response::with(status::NotModified))
} else {
self.response_with_cache(req, path, size, last_modified_time)
}
}
fn response_with_cache<P: AsRef<Path>>(&self,
req: &mut Request,
path: P,
size: u64,
modified: Timespec) -> IronResult<Response> {
use iron::headers::{CacheControl, LastModified, CacheDirective, HttpDate};
use iron::headers::{ContentLength, ContentType, ETag, EntityTag};
use iron::method::Method;
use iron::mime::{Mime, TopLevel, SubLevel};
use iron::modifiers::Header;
let seconds = self.duration.as_secs() as u32;
let cache = vec![CacheDirective::Public, CacheDirective::MaxAge(seconds)];
let metadata = fs::metadata(path.as_ref());
let metadata = try!(metadata.map_err(|e| IronError::new(e, status::InternalServerError)));
let mut response = if req.method == Method::Head {
let has_ct = req.headers.get::<ContentType>();
let cont_type = match has_ct {
None => ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![])),
Some(t) => t.clone()
};
Response::with((status::Ok, Header(cont_type), Header(ContentLength(metadata.len()))))
} else {
Response::with((status::Ok, path.as_ref()))
};
response.headers.set(CacheControl(cache));
response.headers.set(LastModified(HttpDate(time::at(modified))));
response.headers.set(ETag(EntityTag::weak(format!("{0:x}-{1:x}.{2:x}", size, modified.sec, modified.nsec))));
Ok(response)
}
}
#[cfg(feature = "cache")]
impl Modifier<Static> for Cache {
fn modify(self, static_handler: &mut Static) {
static_handler.cache = Some(self);
}
}
#[derive(Debug)]
pub struct NoFile;
impl Error for NoFile {
fn description(&self) -> &str { "File not found" }
}
impl fmt::Display for NoFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.description())
}
}