diff --git a/src/fs.rs b/src/fs.rs index 75650a05..86b9ed55 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -5,13 +5,189 @@ use std::io; use std::io::Read; use std::fmt::Write; use std::fs::{File, DirEntry}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::ops::{Deref, DerefMut}; use mime_guess::get_mime_type; -use route::Handler; +use route::{Handler, FromRequest}; +use recognizer::FromParam; use httprequest::HttpRequest; use httpresponse::HttpResponse; -use httpcodes::{HTTPOk, HTTPNotFound}; +use httpcodes::HTTPOk; + +/// A file with an associated name; responds with the Content-Type based on the +/// file extension. +#[derive(Debug)] +pub struct NamedFile(PathBuf, File); + +impl NamedFile { + /// Attempts to open a file in read-only mode. + /// + /// # Examples + /// + /// ```rust + /// use actix_web::fs::NamedFile; + /// + /// # #[allow(unused_variables)] + /// let file = NamedFile::open("foo.txt"); + /// ``` + pub fn open>(path: P) -> io::Result { + let file = File::open(path.as_ref())?; + Ok(NamedFile(path.as_ref().to_path_buf(), file)) + } + + /// Returns reference to the underlying `File` object. + #[inline] + pub fn file(&self) -> &File { + &self.1 + } + + /// Retrieve the path of this file. + /// + /// # Examples + /// + /// ```rust + /// # use std::io; + /// use actix_web::fs::NamedFile; + /// + /// # #[allow(dead_code)] + /// # fn path() -> io::Result<()> { + /// let file = NamedFile::open("test.txt")?; + /// assert_eq!(file.path().as_os_str(), "foo.txt"); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn path(&self) -> &Path { + self.0.as_path() + } +} + +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &File { + &self.1 + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut File { + &mut self.1 + } +} + +impl FromRequest for NamedFile { + type Item = HttpResponse; + type Error = io::Error; + + fn from_request(mut self, _: HttpRequest) -> Result { + let mut resp = HTTPOk.build(); + if let Some(ext) = self.path().extension() { + let mime = get_mime_type(&ext.to_string_lossy()); + resp.content_type(format!("{}", mime).as_str()); + } + let mut data = Vec::new(); + let _ = self.1.read_to_end(&mut data); + Ok(resp.body(data).unwrap()) + } +} + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory{ + base: PathBuf, + path: PathBuf +} + +impl Directory { + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { + base: base, + path: path + } + } + + 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 FromRequest for Directory { + type Item = HttpResponse; + type Error = io::Error; + + fn from_request(self, req: HttpRequest) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in self.path.read_dir()? { + if self.can_list(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&self.base) { + Ok(p) => base.join(p), + Err(_) => continue + }; + // show file url as relative to static path + let file_url = format!("{}", p.to_string_lossy()); + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!(body, "
  • {}/
  • ", + file_url, entry.file_name().to_string_lossy()); + } else { + let _ = write!(body, "
  • {}
  • ", + file_url, entry.file_name().to_string_lossy()); + } + } else { + continue + } + } + } + + let html = format!("\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", index_of, index_of, body); + Ok(HTTPOk.build() + .content_type("text/html; charset=utf-8") + .body(html).unwrap()) + } +} + +/// This enum represents all filesystem elements. +pub enum FilesystemElement { + File(NamedFile), + Directory(Directory), +} + +impl FromRequest for FilesystemElement { + type Item = HttpResponse; + type Error = io::Error; + + fn from_request(self, req: HttpRequest) -> Result { + match self { + FilesystemElement::File(file) => file.from_request(req), + FilesystemElement::Directory(dir) => dir.from_request(req), + } + } +} + /// Static files handling /// @@ -67,106 +243,25 @@ impl StaticFiles { } } - fn index(&self, prefix: &str, relpath: &str, filename: &PathBuf) -> Result { - let index_of = format!("Index of {}{}", 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!( - "{}{}", prefix, - entry.path().strip_prefix(&self.directory).unwrap().to_string_lossy()); - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - //format!("
  • {}
  • ", file_url, file_name)); - let _ = write!(body, "
  • {}/
  • ", - file_url, entry.file_name().to_string_lossy()); - } else { - // write!(body, "{}/", entry.file_name()) - let _ = write!(body, "
  • {}
  • ", - file_url, entry.file_name().to_string_lossy()); - } - } else { - continue - } - } - } - - let html = format!("\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", index_of, index_of, body); - Ok( - HTTPOk.build() - .content_type("text/html; charset=utf-8") - .body(html).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 Handler for StaticFiles { - type Result = Result; + type Result = Result; fn handle(&self, req: HttpRequest) -> Self::Result { if !self.accessible { - Ok(HTTPNotFound.into()) + Err(io::Error::new(io::ErrorKind::NotFound, "not found")) } else { - let mut hidden = false; - let filepath = req.path()[req.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 Ok(HTTPNotFound.into()) - } + let relpath = PathBuf::from_param(&req.path()[req.prefix_len()..]) + .map_err(|_| io::Error::new(io::ErrorKind::NotFound, "not found"))?; // full filepath - let idx = if filepath.starts_with('/') { 1 } else { 0 }; - let filename = self.directory.join(&filepath[idx..]).canonicalize()?; + let path = self.directory.join(&relpath).canonicalize()?; - if filename.is_dir() { - self.index(&req.path()[..req.prefix_len()], &filepath[idx..], &filename) + if path.is_dir() { + Ok(FilesystemElement::Directory(Directory::new(self.directory.clone(), path))) } else { - let mut resp = HTTPOk.build(); - 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); - Ok(resp.body(data).unwrap()) - }, - Err(err) => Err(err), - } + Ok(FilesystemElement::File(NamedFile::open(path)?)) } } } diff --git a/src/route.rs b/src/route.rs index 7a1fbcbc..cb35da70 100644 --- a/src/route.rs +++ b/src/route.rs @@ -101,27 +101,22 @@ impl From for Reply { } } -impl, E: Into> FromRequest for Result { - type Item = Reply; - type Error = E; +impl> FromRequest for Result +{ + type Item = ::Item; + type Error = Error; - fn from_request(self, _: HttpRequest) -> Result { + fn from_request(self, req: HttpRequest) -> Result { match self { - Ok(val) => Ok(Reply(ReplyItem::Message(val.into()))), - Err(err) => Err(err), + Ok(val) => match val.from_request(req) { + Ok(val) => Ok(val), + Err(err) => Err(err.into()), + }, + Err(err) => Err(err.into()), } } } -impl> FromRequest for Result { - type Item = Reply; - type Error = E; - - fn from_request(self, _: HttpRequest) -> Result { - self - } -} - impl> From> for Reply { fn from(res: Result) -> Self { match res {