use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "compio")]
use compio::fs;
use http::StatusCode;
#[cfg(not(feature = "compio"))]
use tokio::fs;
use crate::body::TakoBody;
use crate::responder::Responder;
use crate::types::Request;
use crate::types::Response;
#[doc(alias = "static")]
#[doc(alias = "serve_dir")]
pub struct ServeDir {
base_dir: PathBuf,
fallback: Option<PathBuf>,
}
#[must_use]
pub struct ServeDirBuilder {
base_dir: PathBuf,
fallback: Option<PathBuf>,
}
impl ServeDirBuilder {
#[inline]
#[must_use]
pub fn new<P: Into<PathBuf>>(base_dir: P) -> Self {
Self {
base_dir: base_dir.into(),
fallback: None,
}
}
#[inline]
#[must_use]
pub fn fallback<P: Into<PathBuf>>(mut self, fallback: P) -> Self {
self.fallback = Some(fallback.into());
self
}
#[inline]
#[must_use]
pub fn build(self) -> ServeDir {
ServeDir {
base_dir: self.base_dir,
fallback: self.fallback,
}
}
}
impl ServeDir {
pub fn builder<P: Into<PathBuf>>(base_dir: P) -> ServeDirBuilder {
ServeDirBuilder::new(base_dir)
}
fn sanitize_path(&self, req_path: &str) -> Option<PathBuf> {
let rel_path = req_path.trim_start_matches('/');
let joined = self.base_dir.join(rel_path);
let canonical = joined.canonicalize().ok()?;
if canonical.starts_with(self.base_dir.canonicalize().ok()?) {
Some(canonical)
} else {
None
}
}
async fn serve_file(&self, file_path: &Path) -> Option<Response> {
match fs::read(file_path).await {
Ok(contents) => {
let mime = mime_guess::from_path(file_path).first_or_octet_stream();
Some(
http::Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, mime.to_string())
.body(TakoBody::from(contents))
.unwrap(),
)
}
Err(_) => None,
}
}
pub async fn handle(&self, req: Request) -> impl Responder {
let path = req.uri().path();
if let Some(file_path) = self.sanitize_path(path)
&& let Some(resp) = self.serve_file(&file_path).await
{
return resp;
}
if let Some(fallback) = &self.fallback
&& let Some(resp) = self.serve_file(fallback).await
{
return resp;
}
http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(TakoBody::from("File not found"))
.unwrap()
}
}
#[doc(alias = "serve_file")]
pub struct ServeFile {
path: PathBuf,
}
#[must_use]
pub struct ServeFileBuilder {
path: PathBuf,
}
impl ServeFileBuilder {
#[inline]
#[must_use]
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self { path: path.into() }
}
#[inline]
#[must_use]
pub fn build(self) -> ServeFile {
ServeFile { path: self.path }
}
}
impl ServeFile {
pub fn builder<P: Into<PathBuf>>(path: P) -> ServeFileBuilder {
ServeFileBuilder::new(path)
}
async fn serve_file(&self) -> Option<Response> {
match fs::read(&self.path).await {
Ok(contents) => {
let mime = mime_guess::from_path(&self.path).first_or_octet_stream();
Some(
http::Response::builder()
.status(StatusCode::OK)
.header(http::header::CONTENT_TYPE, mime.to_string())
.body(TakoBody::from(contents))
.unwrap(),
)
}
Err(_) => None,
}
}
pub async fn handle(&self, _req: Request) -> impl Responder {
if let Some(resp) = self.serve_file().await {
resp
} else {
http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(TakoBody::from("File not found"))
.unwrap()
}
}
}