1use axum::{
2 extract::Path,
3 http::{header, HeaderMap, StatusCode},
4 response::{IntoResponse, Response},
5};
6use rust_embed::Embed;
7
8pub fn asset<A: Embed>(name: &str) -> String {
10 let file = A::get(name).unwrap_or_else(|| panic!("Missing asset: {name}"));
11 String::from_utf8(file.data.to_vec()).unwrap_or_else(|_| panic!("Non-UTF8 asset: {name}"))
12}
13
14pub fn asset_bytes<A: Embed>(name: &str) -> Option<Vec<u8>> {
16 A::get(name).map(|f| f.data.to_vec())
17}
18
19pub fn load_manifest<A: Embed>() -> crate::AssetManifest {
21 let data = asset::<A>("manifest.json");
22 serde_json::from_str(&data).expect("Failed to parse manifest.json")
23}
24
25pub async fn serve_asset<A: Embed>(
29 Path(filename): Path<String>,
30 headers: HeaderMap,
31) -> Response {
32 let accept = headers
33 .get(header::ACCEPT_ENCODING)
34 .and_then(|v| v.to_str().ok())
35 .unwrap_or("");
36
37 let (data, encoding) = if accept.contains("br") {
39 if let Some(br) = A::get(&format!("{filename}.br")) {
40 (br.data.to_vec(), Some("br"))
41 } else if let Some(raw) = A::get(&filename) {
42 (raw.data.to_vec(), None)
43 } else {
44 return StatusCode::NOT_FOUND.into_response();
45 }
46 } else if accept.contains("gzip") {
47 if let Some(gz) = A::get(&format!("{filename}.gz")) {
48 (gz.data.to_vec(), Some("gzip"))
49 } else if let Some(raw) = A::get(&filename) {
50 (raw.data.to_vec(), None)
51 } else {
52 return StatusCode::NOT_FOUND.into_response();
53 }
54 } else if let Some(raw) = A::get(&filename) {
55 (raw.data.to_vec(), None)
56 } else {
57 return StatusCode::NOT_FOUND.into_response();
58 };
59
60 asset_response(&filename, data, encoding)
61}
62
63fn asset_response(filename: &str, data: Vec<u8>, encoding: Option<&str>) -> Response {
64 let mime = mime_for(filename);
65 let mut builder = Response::builder()
66 .header(header::CONTENT_TYPE, mime)
67 .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
68 .header("vary", "accept-encoding")
69 .header("x-content-type-options", "nosniff");
70
71 if let Some(enc) = encoding {
72 builder = builder.header(header::CONTENT_ENCODING, enc);
73 }
74
75 builder
76 .body(axum::body::Body::from(data))
77 .unwrap()
78 .into_response()
79}
80
81fn mime_for(filename: &str) -> &'static str {
82 if filename.ends_with(".js") {
83 "application/javascript; charset=utf-8"
84 } else if filename.ends_with(".css") {
85 "text/css; charset=utf-8"
86 } else if filename.ends_with(".woff2") {
87 "font/woff2"
88 } else if filename.ends_with(".wasm") {
89 "application/wasm"
90 } else if filename.ends_with(".json") {
91 "application/json"
92 } else if filename.ends_with(".html") {
93 "text/html; charset=utf-8"
94 } else if filename.ends_with(".svg") {
95 "image/svg+xml"
96 } else {
97 "application/octet-stream"
98 }
99}