#![warn(missing_docs)]
#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
#[cfg(feature = "hyper")]
pub extern crate hyper;
pub extern crate http;
pub extern crate etag;
pub extern crate httpdate;
pub mod config;
pub mod headers;
pub mod file;
mod body;
pub mod utils;
pub mod adaptors;
use http::header::{self, HeaderValue, HeaderMap};
use http::{Method, StatusCode, Uri};
use percent_encoding::percent_decode;
pub use config::{FileServeConfig, DirectoryListingConfig, StaticFileConfig, FsTaskSpawner};
pub use body::Body;
use core::fmt;
use std::{fs, io};
use std::path::{PathBuf, Path};
use std::borrow::Cow;
#[cold]
#[inline(never)]
fn unexpected_error<T: fmt::Display, W: FsTaskSpawner, C: FileServeConfig>(error: T, response: &mut http::Response<Body<W, C>>) {
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
*response.body_mut() = Body::Full(error.to_string().into())
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum ServeEntry {
NotFound,
IoError(io::Error),
File(fs::File, fs::Metadata, PathBuf),
Directory(PathBuf, fs::ReadDir),
}
pub struct StaticFiles<W, C> {
worker: W,
config: C,
}
impl<W: Clone, C: Clone> Clone for StaticFiles<W, C> {
fn clone(&self) -> StaticFiles<W, C> {
Self {
worker: self.worker.clone(),
config: self.config.clone(),
}
}
}
impl<W: FsTaskSpawner> StaticFiles<W, config::DefaultConfig> {
pub fn default_with(worker: W) -> Self {
Self {
worker,
config: config::DefaultConfig,
}
}
}
impl<W: FsTaskSpawner, C: StaticFileConfig> StaticFiles<W, C> {
pub fn new(worker: W, config: C) -> Self {
Self {
worker,
config,
}
}
pub fn serve(&self, path: &Path) -> ServeEntry {
let mut full_path = self.config.serve_dir().join(path);
let mut meta = match full_path.metadata() {
Ok(meta) => meta,
Err(_) => return ServeEntry::NotFound,
};
if meta.is_dir() {
if let Some(name) = self.config.index_file(path) {
full_path = full_path.join(name);
meta = match full_path.metadata() {
Ok(meta) => meta,
Err(_) => return ServeEntry::NotFound,
};
} else if self.config.handle_directory(path) {
return match full_path.read_dir() {
Ok(dir) => ServeEntry::Directory(path.to_path_buf(), dir),
Err(error) => ServeEntry::IoError(error),
}
} else {
return ServeEntry::NotFound
}
}
match fs::File::open(&full_path) {
Ok(file) => ServeEntry::File(file, meta, full_path),
Err(error) => ServeEntry::IoError(error),
}
}
pub fn handle_not_found(&self, path: &Path, out_headers: &mut http::HeaderMap) -> (StatusCode, bytes::Bytes) {
self.config.handle_not_found(path, out_headers)
}
pub fn list_dir(&self, path: &Path, dir: fs::ReadDir) -> bytes::Bytes {
C::DirService::create_body(self.config.serve_dir(), path, dir)
}
pub fn handle_dir(&self, path: &Path, dir: fs::ReadDir, out_headers: &mut http::HeaderMap) -> bytes::Bytes {
const HTML: HeaderValue = HeaderValue::from_static("text/html; charset=utf-8");
let body = C::DirService::create_body(self.config.serve_dir(), path, dir);
out_headers.insert(header::CONTENT_TYPE, HTML);
out_headers.insert(header::CONTENT_LENGTH, body.len().into());
body
}
pub fn serve_file(&self, path: &Path, file: fs::File, meta: fs::Metadata, method: http::Method, headers: &http::HeaderMap, out_headers: &mut http::HeaderMap) -> (StatusCode, Body<W, C::FileService>) {
let file_name = match path.file_name().and_then(|file_name| file_name.to_str()) {
Some(file_name) => file_name,
None => return (StatusCode::NOT_FOUND, Body::empty())
};
file::ServeFile::<W, C::FileService>::from_parts_with_cfg(file_name, file, meta).prepare(path, method, headers, out_headers)
}
pub fn serve_http(&self, method: &Method, uri: &Uri, headers: &HeaderMap) -> http::Response<Body<W, C::FileService>> {
const ALLOWED: HeaderValue = HeaderValue::from_static("GET, HEAD");
let mut response = http::Response::new(Body::empty());
if !C::is_method_allowed(method) {
*response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
response.headers_mut().insert(header::ALLOW, ALLOWED);
} else {
let path = uri.path().trim_start_matches('/');
let path = match percent_decode(path.as_bytes()).decode_utf8() {
Ok(path) => path,
Err(unexpected) => {
unexpected_error(unexpected, &mut response);
return response;
}
};
let path = &path;
let path = match path {
Cow::Borrowed(path) => Path::new(path),
Cow::Owned(ref path) => Path::new(path),
};
match self.serve(&path) {
ServeEntry::NotFound | ServeEntry::IoError(_) => {
let (code, body) = self.handle_not_found(&path, response.headers_mut());
*response.status_mut() = code;
*response.body_mut() = body.into();
},
ServeEntry::Directory(path, dir) => {
*response.status_mut() = StatusCode::OK;
let body = self.handle_dir(&path, dir, response.headers_mut());
*response.body_mut() = body.into();
},
ServeEntry::File(file, meta, path) => {
let (code, body) = self.serve_file(&path, file, meta, method.clone(), headers, response.headers_mut());
*response.status_mut() = code;
*response.body_mut() = body;
}
}
}
response
}
}
#[cfg(feature = "tokio")]
impl Default for StaticFiles<config::TokioWorker, config::DefaultConfig> {
#[inline]
fn default() -> Self {
Self::default_with(config::TokioWorker)
}
}