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>(Path(filename): Path<String>, headers: HeaderMap) -> Response {
29 let accept = headers
30 .get(header::ACCEPT_ENCODING)
31 .and_then(|v| v.to_str().ok())
32 .unwrap_or("");
33
34 let (data, encoding) = if accept.contains("br") {
36 if let Some(br) = A::get(&format!("{filename}.br")) {
37 (br.data.to_vec(), Some("br"))
38 } else if let Some(raw) = A::get(&filename) {
39 (raw.data.to_vec(), None)
40 } else {
41 return StatusCode::NOT_FOUND.into_response();
42 }
43 } else if accept.contains("gzip") {
44 if let Some(gz) = A::get(&format!("{filename}.gz")) {
45 (gz.data.to_vec(), Some("gzip"))
46 } else if let Some(raw) = A::get(&filename) {
47 (raw.data.to_vec(), None)
48 } else {
49 return StatusCode::NOT_FOUND.into_response();
50 }
51 } else if let Some(raw) = A::get(&filename) {
52 (raw.data.to_vec(), None)
53 } else {
54 return StatusCode::NOT_FOUND.into_response();
55 };
56
57 asset_response(&filename, data, encoding)
58}
59
60fn asset_response(filename: &str, data: Vec<u8>, encoding: Option<&str>) -> Response {
61 let mime = mime_for(filename);
62 let mut builder = Response::builder()
63 .header(header::CONTENT_TYPE, mime)
64 .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
65 .header("vary", "accept-encoding")
66 .header("x-content-type-options", "nosniff");
67
68 if let Some(enc) = encoding {
69 builder = builder.header(header::CONTENT_ENCODING, enc);
70 }
71
72 builder
73 .body(axum::body::Body::from(data))
74 .unwrap()
75 .into_response()
76}
77
78fn mime_for(filename: &str) -> &'static str {
79 if filename.ends_with(".js") {
80 "application/javascript; charset=utf-8"
81 } else if filename.ends_with(".css") {
82 "text/css; charset=utf-8"
83 } else if filename.ends_with(".woff2") {
84 "font/woff2"
85 } else if filename.ends_with(".wasm") {
86 "application/wasm"
87 } else if filename.ends_with(".json") {
88 "application/json"
89 } else if filename.ends_with(".html") {
90 "text/html; charset=utf-8"
91 } else if filename.ends_with(".svg") {
92 "image/svg+xml"
93 } else {
94 "application/octet-stream"
95 }
96}