use super::{Controller, Error};
use crate::http::{Body, Handler, Request, Response};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use async_trait::async_trait;
use time::Duration;
use tokio::fs::File;
use tracing::debug;
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum CacheControl {
NoStore,
MaxAge(Duration),
Private,
NoCache,
}
impl std::fmt::Display for CacheControl {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use CacheControl::*;
let s = match self {
NoStore => "no-store".into(),
MaxAge(duration) => format!("max-age={}", duration.whole_seconds()),
Private => "private".into(),
NoCache => "no-cache".into(),
};
write!(f, "{}", s)
}
}
pub struct StaticFiles {
prefix: PathBuf,
root: PathBuf,
preloads: HashMap<PathBuf, Body>,
cache_control: CacheControl,
}
impl StaticFiles {
pub fn serve(path: &str) -> std::io::Result<Handler> {
let statics = Self::new(path)?;
Ok(statics.handler())
}
pub fn new(path: &str) -> std::io::Result<Self> {
let root_path = Path::new(path);
let root = if root_path.is_absolute() {
root_path.to_owned()
} else {
let cwd = std::env::current_dir()?;
cwd.join(root_path).to_owned()
};
let statics = Self {
prefix: PathBuf::from("/").join(path),
root,
preloads: HashMap::new(),
cache_control: CacheControl::NoStore,
};
Ok(statics)
}
pub fn cached(path: &str, duration: Duration) -> std::io::Result<Handler> {
Ok(Self::new(path)?
.cache_control(CacheControl::MaxAge(duration))
.handler())
}
pub fn preload(mut self, path: impl AsRef<Path> + Copy, bytes: &[u8]) -> Self {
self.preloads.insert(
path.as_ref().to_owned(),
Body::file_include(&path.as_ref().to_owned(), bytes.to_vec()),
);
self
}
pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
self.cache_control = cache_control;
self
}
pub fn prefix(mut self, prefix: &str) -> Self {
self.prefix = PathBuf::from(prefix);
self
}
pub fn handler(self) -> Handler {
Handler::wildcard(self.prefix.display().to_string().as_str(), self)
}
}
#[async_trait]
impl Controller for StaticFiles {
async fn handle(&self, request: &Request) -> Result<Response, Error> {
let path = request.path().to_std();
if let Some(body) = self.preloads.get(&path) {
return Ok(Response::new().body(body.clone()));
}
let path_components = path.components();
let mut prefix_components = self.prefix.components();
let path = path_components
.filter(|path_component| {
if let Some(prefix_component) = prefix_components.next() {
*path_component != prefix_component
} else {
true
}
})
.collect::<PathBuf>();
let path = PathBuf::from(self.root.join(path));
debug!("{} -> {}", request.path().path(), path.display());
let path = match tokio::fs::canonicalize(&path).await {
Ok(path) => path,
Err(_) => {
return Ok(Response::not_found());
}
};
if !path.starts_with(&self.root) {
return Ok(Response::not_found());
}
match File::open(&path).await {
Ok(file) => {
let metadata = match file.metadata().await {
Ok(metadata) => metadata,
Err(err) => return Ok(Response::internal_error(err)),
};
if !metadata.is_file() {
return Ok(Response::not_found());
}
let response =
Response::new().header("cache-control", self.cache_control.to_string());
Ok(response.body((path, file, metadata)))
}
Err(_) => return Ok(Response::not_found()),
}
}
}