use std::path::PathBuf as FsPathBuf;
use http::{
HeaderValue, Method, StatusCode,
header::{CACHE_CONTROL, CONTENT_TYPE},
};
use topcoat_core::context::Cx;
use topcoat_router::{
Body, Methods, Path, PathBuf, Route, RouteFuture, RouteId, response::Response,
};
use crate::BundledAsset;
pub(crate) const ASSET_ROUTE_PREFIX: &str = "/_topcoat/assets";
const CACHE_CONTROL_VALUE: HeaderValue =
HeaderValue::from_static("public, max-age=31536000, immutable");
#[derive(Debug, Clone)]
pub struct AssetRoute {
id: RouteId,
path: PathBuf,
file: FsPathBuf,
content_type: HeaderValue,
}
impl AssetRoute {
#[must_use]
#[track_caller]
pub fn new(dir: &std::path::Path, asset: &BundledAsset) -> Self {
let name = asset.name();
let content_type = HeaderValue::from_str(asset.content_type()).unwrap_or_else(|_| {
panic!(
"asset `{}` has Content-Type \"{}\" that cannot be converted into a header value",
name,
asset.content_type()
)
});
Self {
id: RouteId::new(),
path: Path::new(&format!("{ASSET_ROUTE_PREFIX}/{name}")).to_owned(),
file: dir.join(name),
content_type,
}
}
}
impl Route for AssetRoute {
fn id(&self) -> RouteId {
self.id
}
fn methods(&self) -> Methods<'_> {
Methods::Only(&[Method::GET])
}
fn path(&self) -> &Path {
&self.path
}
fn handle<'cx>(&'cx self, _cx: &'cx Cx, _body: Body) -> RouteFuture<'cx> {
Box::pin(async move {
let response = if let Ok(bytes) = tokio::fs::read(&self.file).await {
let mut response = Response::new(Body::from(bytes));
let headers = response.headers_mut();
headers.insert(CONTENT_TYPE, self.content_type.clone());
headers.insert(CACHE_CONTROL, CACHE_CONTROL_VALUE);
response
} else {
let mut response = Response::new(Body::empty());
*response.status_mut() = StatusCode::NOT_FOUND;
response
};
Ok(response)
})
}
}