use {
crate::{
error::Error,
future::TryFuture,
handler::ModifyHandler,
input::Input,
output::{IntoResponse, ResponseBody},
responder::Responder,
},
bytes::{BufMut, Bytes, BytesMut},
filetime::FileTime,
futures01::{Async, Poll, Stream},
http::{
header::{self, HeaderMap},
Request, Response, StatusCode,
},
log::trace,
mime::Mime,
std::{
borrow::Cow,
cmp, fmt,
fs::{File, Metadata},
io::{self, Read as _Read},
mem,
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::Duration,
},
time::Timespec,
tokio_threadpool::blocking as poll_blocking,
};
fn parse_http_date(s: &str) -> Result<Timespec, time::ParseError> {
time::strptime(s, "%a, %d %b %Y %T %Z")
.or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z"))
.or_else(|_| time::strptime(s, "%c"))
.map(|tm| tm.to_timespec())
}
#[derive(Debug)]
struct ETag {
weak: bool,
tag: String,
}
impl ETag {
fn from_metadata(metadata: &Metadata) -> Self {
let last_modified = FileTime::from_last_modification_time(&metadata);
Self {
weak: true,
tag: format!(
"{:x}-{:x}.{:x}",
metadata.len(),
last_modified.seconds(),
last_modified.nanoseconds()
),
}
}
fn parse_inner(weak: bool, s: &str) -> Result<Self, failure::Error> {
if s.len() < 2 {
failure::bail!("");
}
if !s.starts_with('"') || !s.ends_with('"') {
failure::bail!("");
}
let tag = &s[1..s.len() - 1];
if !tag.is_ascii() {
failure::bail!("");
}
Ok(Self {
weak,
tag: tag.to_owned(),
})
}
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag && (self.weak || !other.weak)
}
}
impl FromStr for ETag {
type Err = failure::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.get(0..3) {
Some("W/\"") if s[2..].starts_with('"') => Self::parse_inner(true, &s[2..]),
Some(t) if t.starts_with('"') => Self::parse_inner(false, s),
Some(..) => failure::bail!("invalid string to parse ETag"),
None => failure::bail!("empty string to parse ETag"),
}
}
}
impl fmt::Display for ETag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weak {
f.write_str("W/")?;
}
write!(f, "\"{}\"", self.tag)
}
}
#[derive(Debug, Default, Clone)]
pub struct OpenConfig {
pub chunk_size: Option<usize>,
pub max_age: Option<Duration>,
}
#[derive(Debug, Clone)]
pub struct NamedFile<P> {
path: P,
config: Option<OpenConfig>,
}
impl<P> NamedFile<P>
where
P: AsRef<Path> + Send + 'static,
{
pub fn open(path: P) -> Self {
Self { path, config: None }
}
pub fn open_with_config(path: P, config: OpenConfig) -> Self {
Self {
path,
config: Some(config),
}
}
}
impl<P> Responder for NamedFile<P>
where
P: AsRef<Path> + Send + 'static,
{
type Response = Response<ResponseBody>;
type Error = crate::Error;
type Respond = OpenNamedFile<P>;
#[inline]
fn respond(self) -> Self::Respond {
OpenNamedFile {
path: self.path,
config: self.config,
}
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct OpenNamedFile<P> {
path: P,
config: Option<OpenConfig>,
}
impl<P> TryFuture for OpenNamedFile<P>
where
P: AsRef<Path>,
{
type Ok = Response<ResponseBody>;
type Error = crate::Error;
fn poll_ready(&mut self, input: &mut Input<'_>) -> Poll<Self::Ok, Self::Error> {
let (file, meta) = futures01::try_ready!(blocking_io(|| {
let file = File::open(&self.path)?;
let meta = file.metadata()?;
Ok((file, meta))
}));
let config = self.config.take().unwrap_or_default();
let last_modified = FileTime::from_last_modification_time(&meta);
let etag = ETag::from_metadata(&meta);
let content_type = mime_guess::guess_mime_type(&self.path);
let response = NamedFileResponse {
file,
meta,
content_type,
last_modified,
etag,
config,
}
.into_response(input.request)?;
Ok(Async::Ready(response))
}
}
#[derive(Debug)]
struct NamedFileResponse {
file: File,
meta: Metadata,
content_type: Mime,
etag: ETag,
last_modified: FileTime,
config: OpenConfig,
}
impl NamedFileResponse {
#[allow(clippy::cast_sign_loss)]
fn is_modified(&self, headers: &HeaderMap) -> Result<bool, Error> {
if let Some(h) = headers.get(header::IF_NONE_MATCH) {
trace!("NamedFile::is_modified(): validate If-None-Match");
let etag: ETag = h
.to_str()
.map_err(crate::error::bad_request)?
.parse()
.map_err(crate::error::bad_request)?;
let modified = !etag.eq(&self.etag);
trace!(
"--> self.etag={:?}, etag={:?}, modified={}",
self.etag,
etag,
modified
);
return Ok(modified);
}
if let Some(h) = headers.get(header::IF_MODIFIED_SINCE) {
trace!("NamedFile::is_modified(): validate If-Modified-Since");
let if_modified_since = {
let timespec = parse_http_date(h.to_str().map_err(crate::error::bad_request)?)
.map_err(crate::error::bad_request)?;
FileTime::from_unix_time(timespec.sec, timespec.nsec as u32)
};
let modified = self.last_modified > if_modified_since;
trace!(
"--> if_modified_sicne={:?}, modified={}",
if_modified_since,
modified
);
return Ok(modified);
}
Ok(true)
}
fn cache_control(&self) -> Cow<'static, str> {
match self.config.max_age {
Some(ref max_age) => format!("public, max-age={}", max_age.as_secs()).into(),
None => "public".into(),
}
}
#[allow(clippy::cast_possible_wrap)]
fn last_modified(&self) -> Result<String, time::ParseError> {
let tm = time::at(Timespec::new(
self.last_modified.seconds(),
self.last_modified.nanoseconds() as i32,
));
time::strftime("%c", &tm)
}
}
impl IntoResponse for NamedFileResponse {
type Body = ResponseBody;
type Error = Error;
fn into_response(self, request: &Request<()>) -> Result<Response<Self::Body>, Self::Error> {
trace!("NamedFile::respond_to");
if !self.is_modified(request.headers())? {
return Ok(Response::builder()
.status(StatusCode::NOT_MODIFIED)
.body(ResponseBody::empty())
.unwrap());
}
let cache_control = self.cache_control();
let last_modified = self
.last_modified()
.map_err(crate::error::internal_server_error)?;
let stream = ReadStream::new(self.file, self.meta, self.config.chunk_size);
Ok(Response::builder()
.header(header::CONTENT_TYPE, self.content_type.as_ref())
.header(header::CACHE_CONTROL, &*cache_control)
.header(header::LAST_MODIFIED, &*last_modified)
.header(header::ETAG, &*self.etag.to_string())
.body(ResponseBody::wrap_stream(stream))
.unwrap())
}
}
#[derive(Debug)]
struct ReadStream(State);
#[derive(Debug)]
enum State {
Reading { file: File, buf_size: usize },
Eof,
Gone,
}
impl ReadStream {
fn new(file: File, meta: Metadata, buf_size: Option<usize>) -> Self {
let buf_size = finalize_block_size(buf_size, &meta);
drop(meta);
ReadStream(State::Reading { file, buf_size })
}
}
impl Stream for ReadStream {
type Item = Bytes;
type Error = io::Error;
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
loop {
match self.0 {
State::Reading {
ref mut file,
buf_size,
..
} => {
trace!("ReadStream::poll(): polling on the mode State::Reading");
let buf = futures01::try_ready!(blocking_io(|| {
let mut buf = BytesMut::with_capacity(buf_size);
if !buf.has_remaining_mut() {
buf.reserve(buf_size);
}
unsafe {
let n = file.read(buf.bytes_mut())?;
buf.advance_mut(n);
}
Ok(buf)
}));
if !buf.is_empty() {
return Ok(Async::Ready(Some(buf.freeze())));
}
}
State::Eof => {
trace!("ReadStream::poll(): polling on the mode State::Reading");
return Ok(Async::Ready(None));
}
State::Gone => panic!("unexpected state"),
};
match mem::replace(&mut self.0, State::Gone) {
State::Reading { .. } => self.0 = State::Eof,
_ => unreachable!("unexpected state"),
}
}
}
}
#[allow(dead_code)]
const DEFAULT_BUF_SIZE: u64 = 8192;
fn blocking_io<T>(f: impl FnOnce() -> io::Result<T>) -> Poll<T, io::Error> {
match poll_blocking(f) {
Ok(Async::Ready(ready)) => ready.map(Async::Ready),
Ok(Async::NotReady) => Ok(Async::NotReady),
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
}
}
#[allow(clippy::cast_possible_truncation)]
fn finalize_block_size(buf_size: Option<usize>, meta: &Metadata) -> usize {
match buf_size {
Some(n) => cmp::min(meta.len(), n as u64) as usize,
None => cmp::min(meta.len(), block_size(&meta)) as usize,
}
}
#[cfg(unix)]
fn block_size(meta: &Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
meta.blksize()
}
#[cfg(not(unix))]
fn block_size(_: &Metadata) -> u64 {
DEFAULT_BUF_SIZE
}
#[derive(Debug, Clone)]
pub struct ArcPath(Arc<PathBuf>);
impl From<PathBuf> for ArcPath {
fn from(path: PathBuf) -> Self {
ArcPath(Arc::new(path))
}
}
impl AsRef<Path> for ArcPath {
fn as_ref(&self) -> &Path {
(*self.0).as_ref()
}
}
impl Deref for ArcPath {
type Target = Path;
#[inline]
fn deref(&self) -> &Self::Target {
(*self.0).as_ref()
}
}
#[derive(Debug, Clone)]
pub struct ServeFile {
inner: Arc<ServeFileInner>,
}
#[derive(Debug)]
struct ServeFileInner {
path: ArcPath,
config: Option<OpenConfig>,
extract_path: bool,
}
mod impl_handler_for_serve_file {
use {
super::{ArcPath, NamedFile, ServeFile},
crate::{
error::Error,
future::TryFuture,
handler::{AllowedMethods, Handler},
input::Input,
},
futures01::{Async, Poll},
};
impl Handler for ServeFile {
type Output = NamedFile<ArcPath>;
type Error = Error;
type Handle = Self;
fn allowed_methods(&self) -> Option<&AllowedMethods> {
Some(&AllowedMethods::get())
}
fn handle(&self) -> Self::Handle {
self.clone()
}
}
impl TryFuture for ServeFile {
type Ok = NamedFile<ArcPath>;
type Error = Error;
fn poll_ready(&mut self, input: &mut Input<'_>) -> Poll<Self::Ok, Self::Error> {
let path = if self.inner.extract_path {
let path = input
.params
.as_ref()
.and_then(|params| params.catch_all())
.ok_or_else(|| crate::error::internal_server_error("missing params"))?;
self.inner.path.join(path).into()
} else {
self.inner.path.clone()
};
Ok(Async::Ready(match self.inner.config {
Some(ref config) => NamedFile::open_with_config(path, config.clone()),
None => NamedFile::open(path),
}))
}
}
}
#[derive(Debug)]
pub struct Staticfiles<P> {
root_dir: P,
config: Option<OpenConfig>,
}
impl<P> Staticfiles<P>
where
P: AsRef<Path>,
{
pub fn new(root_dir: P) -> Self {
Self {
root_dir,
config: None,
}
}
pub fn open_config(self, config: OpenConfig) -> Self {
Self {
config: Some(config),
..self
}
}
}
impl<P, M, C> crate::config::Config<M, C> for Staticfiles<P>
where
P: AsRef<Path>,
M: ModifyHandler<ServeFile>,
M::Handler: Into<C::Handler>,
C: crate::app::config::Concurrency,
{
type Error = crate::config::Error;
fn configure(self, scope: &mut crate::app::config::Scope<'_, M, C>) -> crate::app::Result<()> {
let Self { root_dir, config } = self;
for entry in std::fs::read_dir(root_dir).map_err(crate::config::Error::custom)? {
let entry = entry.map_err(crate::config::Error::custom)?;
let name = entry.file_name();
let name = name
.to_str() .ok_or_else(|| {
crate::config::Error::custom(failure::format_err!("the filename must be UTF-8"))
})?;
let path = entry
.path()
.canonicalize()
.map(|path| ArcPath(Arc::new(path)))
.map_err(crate::config::Error::custom)?;
let file_type = entry.file_type().map_err(crate::config::Error::custom)?;
if file_type.is_file() {
scope.route(
format!("/{}", name),
ServeFile {
inner: Arc::new(ServeFileInner {
path,
config: config.clone(),
extract_path: false,
}),
},
)?;
} else if file_type.is_dir() {
scope.route(
format!("/{}/*path", name),
ServeFile {
inner: Arc::new(ServeFileInner {
path,
config: config.clone(),
extract_path: true,
}),
},
)?;
} else {
return Err(crate::config::Error::custom(failure::format_err!(
"unexpected file type"
)));
}
}
Ok(())
}
}