use std::path::PathBuf as FsPathBuf;
use http::header::{CACHE_CONTROL, CONTENT_TYPE};
use http::{HeaderValue, Method, StatusCode};
use topcoat_core::context::Cx;
use topcoat_router::{Body, Path, PathBuf, Response, Route, RouteFuture, RouterBuilder};
use crate::{AssetBundle, AssetRouteResolver, BundledAsset};
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 {
path: PathBuf,
file: FsPathBuf,
content_type: HeaderValue,
}
impl AssetRoute {
#[must_use]
pub fn new(asset: &BundledAsset) -> Self {
let name = asset.name().to_str().expect("asset had non-UTF8 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 {
path: Path::new(&format!("{ASSET_ROUTE_PREFIX}/{name}")).to_owned(),
file: asset.path().to_path_buf(),
content_type,
}
}
}
impl Route for AssetRoute {
fn method(&self) -> Method {
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)
})
}
}
pub trait RouterBuilderAssetExt {
#[must_use]
fn assets(self, bundle: AssetBundle) -> Self;
}
impl RouterBuilderAssetExt for RouterBuilder {
fn assets(mut self, bundle: AssetBundle) -> Self {
for asset in bundle.assets() {
self = self.route(AssetRoute::new(asset));
}
self = self.app_context(bundle);
self = self.app_context(AssetRouteResolver::new(Box::new(|bundled_asset, write| {
write.write_str(ASSET_ROUTE_PREFIX)?;
write.write_str("/")?;
write.write_str(
bundled_asset
.name()
.to_str()
.expect("asset needs UTF-8 name"),
)?;
Ok(())
})));
self
}
}