use std::path::{PathBuf, Path};
use crate::{Request, Data};
use crate::http::{Method, Status, uri::Segments, ext::IntoOwned};
use crate::route::{Route, Handler, Outcome};
use crate::response::{Redirect, Responder};
use crate::outcome::IntoOutcome;
use crate::fs::NamedFile;
#[derive(Debug, Clone)]
pub struct FileServer {
root: PathBuf,
options: Options,
rank: isize,
}
impl FileServer {
const DEFAULT_RANK: isize = 10;
#[track_caller]
pub fn from<P: AsRef<Path>>(path: P) -> Self {
FileServer::new(path, Options::default())
}
#[track_caller]
pub fn new<P: AsRef<Path>>(path: P, options: Options) -> Self {
use crate::yansi::Paint;
let path = path.as_ref();
if !options.contains(Options::Missing) {
if !options.contains(Options::IndexFile) && !path.is_dir() {
let path = path.display();
error!("FileServer path '{}' is not a directory.", path.primary());
warn_!("Aborting early to prevent inevitable handler error.");
panic!("invalid directory: refusing to continue");
} else if !path.exists() {
let path = path.display();
error!("FileServer path '{}' is not a file.", path.primary());
warn_!("Aborting early to prevent inevitable handler error.");
panic!("invalid file: refusing to continue");
}
}
FileServer { root: path.into(), options, rank: Self::DEFAULT_RANK }
}
pub fn rank(mut self, rank: isize) -> Self {
self.rank = rank;
self
}
}
impl From<FileServer> for Vec<Route> {
fn from(server: FileServer) -> Self {
let source = figment::Source::File(server.root.clone());
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
route.name = Some(format!("FileServer: {}", source).into());
vec![route]
}
}
#[crate::async_trait]
impl Handler for FileServer {
async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
use crate::http::uri::fmt::Path;
let options = self.options;
if options.contains(Options::IndexFile) && self.root.is_file() {
let segments = match req.segments::<Segments<'_, Path>>(0..) {
Ok(segments) => segments,
Err(never) => match never {},
};
if segments.is_empty() {
let file = NamedFile::open(&self.root).await;
return file.respond_to(req).or_forward((data, Status::NotFound));
} else {
return Outcome::forward(data, Status::NotFound);
}
}
let allow_dotfiles = options.contains(Options::DotFiles);
let path = req.segments::<Segments<'_, Path>>(0..).ok()
.and_then(|segments| segments.to_path_buf(allow_dotfiles).ok())
.map(|path| self.root.join(path));
match path {
Some(p) if p.is_dir() => {
if options.contains(Options::NormalizeDirs) && !req.uri().path().ends_with('/') {
let normal = req.uri().map_path(|p| format!("{}/", p))
.expect("adding a trailing slash to a known good path => valid path")
.into_owned();
return Redirect::permanent(normal)
.respond_to(req)
.or_forward((data, Status::InternalServerError));
}
if !options.contains(Options::Index) {
return Outcome::forward(data, Status::NotFound);
}
let index = NamedFile::open(p.join("index.html")).await;
index.respond_to(req).or_forward((data, Status::NotFound))
},
Some(p) => {
let file = NamedFile::open(p).await;
file.respond_to(req).or_forward((data, Status::NotFound))
}
None => Outcome::forward(data, Status::NotFound),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Options(u8);
#[allow(non_upper_case_globals, non_snake_case)]
impl Options {
pub const None: Options = Options(0);
pub const Index: Options = Options(1 << 0);
pub const DotFiles: Options = Options(1 << 1);
pub const NormalizeDirs: Options = Options(1 << 2);
pub const IndexFile: Options = Options(1 << 3);
pub const Missing: Options = Options(1 << 4);
#[inline]
pub fn contains(self, other: Options) -> bool {
(other.0 & self.0) == other.0
}
}
impl Default for Options {
fn default() -> Self {
Options::Index
}
}
impl std::ops::BitOr for Options {
type Output = Self;
#[inline(always)]
fn bitor(self, rhs: Self) -> Self {
Options(self.0 | rhs.0)
}
}
crate::export! {
macro_rules! relative {
($path:expr) => {
if cfg!(windows) {
concat!(env!("CARGO_MANIFEST_DIR"), "\\", $path)
} else {
concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)
}
};
}
}