pd/
zipserve.rs

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    // Create a span describing the router to wrap request handling with.
13    let span = tracing::error_span!("zipserve", prefix = prefix);
14    let span1 = span.clone();
15    let span2 = span.clone();
16    // Per Axum docs, wildcard captures don't match empty segments, so we need
17    // a special route for the route path.
18    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    // Rewrite paths ending in / to /index.html
38    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        // Make a best-guess at the mime-type or else use octet-stream
48        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}