use std::path::PathBuf;
use std::sync::Arc;
use axum::body::Body;
use axum::http::{HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
#[derive(Clone, Debug)]
pub struct Pages {
inner: Arc<PagesInner>,
}
#[derive(Debug)]
struct PagesInner {
root: PathBuf,
index: Option<String>,
}
impl Pages {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
inner: Arc::new(PagesInner {
root: root.into(),
index: Some("index.html".to_string()),
}),
}
}
#[must_use]
pub fn without_index(root: impl Into<PathBuf>) -> Self {
Self {
inner: Arc::new(PagesInner {
root: root.into(),
index: None,
}),
}
}
#[must_use]
pub fn root(&self) -> &std::path::Path {
&self.inner.root
}
pub async fn serve(&self, relative: &str) -> Response {
let path = std::path::Path::new(relative);
if path.is_absolute()
|| path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return StatusCode::FORBIDDEN.into_response();
}
let full = self.inner.root.join(path);
let full = if full.is_dir() {
if let Some(index) = &self.inner.index {
full.join(index)
} else {
return StatusCode::FORBIDDEN.into_response();
}
} else {
full
};
match tokio::fs::read(&full).await {
Ok(bytes) => {
let mime = mime_for(&full);
let mut response = Response::new(Body::from(bytes));
*response.status_mut() = StatusCode::OK;
if let Ok(value) = HeaderValue::from_str(mime) {
response
.headers_mut()
.insert(axum::http::header::CONTENT_TYPE, value);
}
response
}
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}
}
fn mime_for(path: &std::path::Path) -> &'static str {
match path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
{
Some("html") | Some("htm") => "text/html; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("js") | Some("mjs") => "application/javascript; charset=utf-8",
Some("json") => "application/json; charset=utf-8",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
Some("svg") => "image/svg+xml",
Some("ico") => "image/x-icon",
Some("woff") => "font/woff",
Some("woff2") => "font/woff2",
Some("ttf") => "font/ttf",
Some("txt") => "text/plain; charset=utf-8",
Some("wasm") => "application/wasm",
_ => "application/octet-stream",
}
}
pub use crate::http::Maintenance as MaintenanceGuard;