use crate::AppError;
use crate::http::response::into_response::{IntoResponse, bytes_to_boxed};
use crate::router::HttpSvc;
use bytes::Bytes;
use http_body_util::BodyExt;
use hyper::{HeaderMap, Method, Response, StatusCode, header};
use miko_core::fallible_stream_body::FallibleStreamBody;
use miko_core::{Req, Resp, decode_path};
use std::future::Future;
use std::io::SeekFrom;
use std::path::{Component, Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio_util::io::ReaderStream;
use tower::util::BoxCloneService;
use tower::{Layer, Service};
use tower_http::cors::CorsLayer;
#[derive(Clone)]
pub struct StaticSvc {
pub root: Arc<PathBuf>,
pub spa_fallback: bool,
pub fallback_files: Arc<Vec<String>>,
pub index_files: Arc<Vec<String>>,
}
impl StaticSvc {
pub fn builder(root: impl Into<PathBuf>) -> StaticSvcBuilder {
StaticSvcBuilder::new(root)
}
fn resolve_path(&self, uri_path: &str) -> PathBuf {
let mut path = self.root.as_ref().clone();
let decoded = decode_path(uri_path);
let safe_rel = Path::new(&decoded)
.components()
.filter(|c| matches!(c, Component::Normal(_)))
.collect::<PathBuf>();
path.push(safe_rel);
path
}
async fn resolve_index_file(&self, path: PathBuf) -> Option<PathBuf> {
if let Ok(metadata) = tokio::fs::metadata(&path).await
&& metadata.is_dir()
{
for index_file in self.index_files.iter() {
let index_path = path.join(index_file);
if tokio::fs::metadata(&index_path).await.is_ok() {
return Some(index_path);
}
}
}
None
}
async fn try_fallback_files(&self, root: &Path) -> Option<PathBuf> {
for fallback_file in self.fallback_files.iter() {
let fallback_path = root.join(fallback_file);
if tokio::fs::metadata(&fallback_path).await.is_ok() {
return Some(fallback_path);
}
}
None
}
fn parse_range(range_header: &str, file_size: u64) -> Result<Option<(u64, u64)>, ()> {
if !range_header.starts_with("bytes=") {
return Ok(None);
}
let range_str = &range_header[6..];
if range_str.contains(',') {
return Ok(None);
}
let parts: Vec<&str> = range_str.split('-').collect();
if parts.len() != 2 {
return Ok(None);
}
if parts[0].is_empty() {
if let Ok(suffix_length) = parts[1].parse::<u64>() {
if suffix_length == 0 {
return Err(());
}
let start = file_size.saturating_sub(suffix_length);
return Ok(Some((start, file_size - 1)));
}
return Ok(None);
}
let start = parts[0].parse::<u64>().map_err(|_| ())?;
if start >= file_size {
return Err(());
}
let end = if parts[1].is_empty() {
file_size - 1
} else {
parts[1].parse::<u64>().map_err(|_| ())?.min(file_size - 1)
};
if start <= end {
Ok(Some((start, end)))
} else {
Err(()) }
}
async fn serve_file(
path: &PathBuf,
method: &Method,
headers: &HeaderMap,
) -> Result<Resp, std::io::Error> {
let mime = mime_guess::from_path(path).first_or_octet_stream();
let content_type = if mime.type_() == mime_guess::mime::TEXT {
format!("{}; charset=utf-8", mime)
} else {
mime.to_string()
};
let metadata = tokio::fs::metadata(path).await?;
let file_size = metadata.len();
let etag = if let Ok(modified) = metadata.modified() {
format!(
"\"{:x}-{:x}\"",
modified
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_size
)
} else {
format!("\"{:x}\"", file_size)
};
if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH)
&& let Ok(if_none_match_str) = if_none_match.to_str()
&& (if_none_match_str == etag || if_none_match_str == "*")
{
return Ok(Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(header::ETAG, etag)
.body(bytes_to_boxed(Bytes::new()))
.unwrap());
}
let mut builder = Response::builder()
.header(header::CONTENT_TYPE, content_type)
.header(header::ETAG, etag)
.header(header::ACCEPT_RANGES, "bytes");
if let Ok(time) = metadata.modified() {
let datetime = httpdate::fmt_http_date(time);
builder = builder.header(header::LAST_MODIFIED, datetime);
}
if let Some(range_header) = headers.get(header::RANGE)
&& let Ok(range_str) = range_header.to_str()
{
match Self::parse_range(range_str, file_size) {
Ok(Some((start, end))) => {
let content_length = end - start + 1;
let mut file = File::open(path).await?;
file.seek(SeekFrom::Start(start)).await?;
builder = builder
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_LENGTH, content_length)
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", start, end, file_size),
);
if method == Method::HEAD {
return Ok(builder.body(bytes_to_boxed(Bytes::new())).unwrap());
}
let limited_file = file.take(content_length);
let stream = ReaderStream::new(limited_file);
let body = FallibleStreamBody::with_size_hint(stream, content_length);
return Ok(builder
.body(body.map_err(Into::into).boxed_unsync())
.unwrap());
}
Err(()) => {
return Ok(Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(header::CONTENT_RANGE, format!("bytes */{}", file_size))
.body(bytes_to_boxed(Bytes::new()))
.unwrap());
}
Ok(None) => {
}
}
}
builder = builder
.status(StatusCode::OK)
.header(header::CONTENT_LENGTH, file_size);
if method == Method::HEAD {
return Ok(builder.body(bytes_to_boxed(Bytes::new())).unwrap());
}
let file = File::open(path).await?;
let stream = ReaderStream::new(file);
let body = FallibleStreamBody::with_size_hint(stream, file_size);
Ok(builder
.body(body.map_err(Into::into).boxed_unsync())
.unwrap())
}
}
pub struct StaticSvcBuilder {
pub root: PathBuf,
pub spa_fallback: bool,
pub fallback_files: Vec<String>,
pub index_files: Vec<String>,
pub cors_layer: Option<CorsLayer>,
}
impl StaticSvcBuilder {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
spa_fallback: false,
fallback_files: vec!["index.html".to_string()],
index_files: vec!["index.html".to_string(), "index.htm".to_string()],
cors_layer: None,
}
}
pub fn with_spa_fallback(mut self, spa_fallback: bool) -> Self {
self.spa_fallback = spa_fallback;
self
}
pub fn with_fallback_files(
mut self,
files: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.fallback_files = files.into_iter().map(|f| f.into()).collect();
self
}
pub fn with_index_files(mut self, files: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.index_files = files.into_iter().map(|f| f.into()).collect();
self
}
pub fn with_cors(mut self, cors_layer: CorsLayer) -> Self {
self.cors_layer = Some(cors_layer);
self
}
pub fn cors_any(mut self) -> Self {
self.cors_layer = Some(CorsLayer::permissive());
self
}
pub fn build(self) -> HttpSvc<Req> {
let service = StaticSvc {
root: Arc::new(self.root),
spa_fallback: self.spa_fallback,
fallback_files: Arc::new(self.fallback_files),
index_files: Arc::new(self.index_files),
};
if let Some(cors_layer) = self.cors_layer {
BoxCloneService::new(cors_layer.clone().layer(service))
} else {
BoxCloneService::new(service)
}
}
}
impl Service<Req> for StaticSvc {
type Response = Resp;
type Error = AppError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Req) -> Self::Future {
let root = self.root.clone();
let spa_fallback = self.spa_fallback;
let mut path = self.resolve_path(req.uri().path());
let (parts, _body) = req.into_parts();
let method = parts.method;
let headers = parts.headers;
let self_clone = self.clone();
Box::pin(async move {
if let Some(index_path) = self_clone.resolve_index_file(path.clone()).await {
path = index_path;
}
match StaticSvc::serve_file(&path, &method, &headers).await {
Ok(resp) => Ok(resp),
Err(e) => {
if spa_fallback
&& e.kind() == std::io::ErrorKind::NotFound
&& let Some(fallback_path) = self_clone.try_fallback_files(&root).await
&& let Ok(resp) =
StaticSvc::serve_file(&fallback_path, &method, &headers).await
{
return Ok(resp);
}
Ok(AppError::NotFound("File not found".to_string()).into_response())
}
}
})
}
}