1use axum::{
2 extract::Path,
3 response::{IntoResponse, Response},
4 routing::get,
5 Router,
6};
7use std::{io::Cursor, io::Read};
8use tracing::Instrument;
9use zip::ZipArchive;
10
11pub fn router(prefix: &str, archive_bytes: &'static [u8]) -> axum::Router {
12 let span = tracing::error_span!("zipserve", prefix = prefix);
14 let span1 = span.clone();
15 let span2 = span.clone();
16 let path1 = format!("{prefix}");
19 assert!(prefix.ends_with("/"), "prefix must end in a /");
20 let path2 = format!("{prefix}*path");
21 let handler1 =
22 move || serve_zip(archive_bytes, Path("index.html".to_string())).instrument(span1);
23 let handler2 = move |path: Path<String>| serve_zip(archive_bytes, path).instrument(span2);
24 Router::new()
25 .route(&path1, get(handler1))
26 .route(&path2, get(handler2))
27}
28
29#[allow(clippy::unwrap_used)]
30async fn serve_zip(
31 archive_bytes: &'static [u8],
32 Path(mut path): Path<String>,
33) -> impl IntoResponse {
34 let cursor = Cursor::new(archive_bytes);
35 let mut archive = ZipArchive::new(cursor).unwrap();
36
37 if path.ends_with('/') {
39 tracing::debug!(orig_path = ?path, "rewriting to index.html");
40 path.push_str("index.html");
41 }
42
43 let rsp = if let Ok(mut file) = archive.by_name(&path) {
44 let mut contents = Vec::new();
45 file.read_to_end(&mut contents).unwrap();
46
47 let mime_type = mime_guess::from_path(&path)
49 .first_or_octet_stream()
50 .to_string();
51
52 tracing::debug!(path = ?path, mime_type = ?mime_type, len = ?contents.len(), "serving file");
53
54 Response::builder()
55 .header("Content-Type", mime_type)
56 .body(axum::body::Body::from(contents))
57 .unwrap()
58 } else {
59 tracing::debug!(path = ?path, "file not found in archive");
60
61 Response::builder()
62 .status(404)
63 .body("File not found in archive".into())
64 .unwrap()
65 };
66
67 rsp
68}