axum_embed_hashed_asset/
lib.rs

1use axum::body::Body;
2use axum::extract::Path;
3use axum::http::{header, StatusCode};
4use axum::response::{IntoResponse, Response};
5use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
6
7const HASH_LEN: usize = 8;
8
9pub fn path<Asset: rust_embed::RustEmbed>(prefix: &str, file_path: &str) -> Option<String> {
10    Asset::get(file_path).map(|f| {
11        let mut p = String::from(prefix);
12        if !prefix.ends_with('/') {
13            p.push('/');
14        }
15        BASE64_URL_SAFE_NO_PAD.encode_string(&f.metadata.sha256_hash()[..HASH_LEN], &mut p);
16        p.push('/');
17        p.push_str(file_path);
18        p
19    })
20}
21
22pub async fn handle<Asset: rust_embed::RustEmbed>(
23    Path(path): Path<String>,
24) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
25    let (hash_b64, file_path) = path
26        .split_once('/')
27        .ok_or((StatusCode::BAD_REQUEST, "invalid asset url"))?;
28    let file = Asset::get(file_path).ok_or((StatusCode::NOT_FOUND, "asset not found"))?;
29    let hash = BASE64_URL_SAFE_NO_PAD
30        .decode(hash_b64)
31        .map_err(|_| (StatusCode::BAD_REQUEST, "hash invalid format"))?;
32    if hash.len() != HASH_LEN {
33        return Err((StatusCode::BAD_REQUEST, "hash invalid length"));
34    }
35    if !file.metadata.sha256_hash().starts_with(&hash) {
36        return Err((StatusCode::BAD_REQUEST, "hash mismatch"));
37    }
38    Ok(Response::builder()
39        .header(header::CONTENT_TYPE, file.metadata.mimetype())
40        .header(header::CACHE_CONTROL, "public,max-age=31536000,immutable")
41        .body(Body::from(file.data))
42        .map_err(|_| {
43            (
44                StatusCode::INTERNAL_SERVER_ERROR,
45                "failed to build response",
46            )
47        }))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use axum::routing::get;
54    use axum::Router;
55    use rust_embed::RustEmbed;
56
57    #[derive(RustEmbed)]
58    #[folder = "src/"]
59    struct Asset;
60
61    #[test]
62    fn test_is_valid_handler() {
63        let _ = Router::<()>::new().route("/", get(handle::<Asset>));
64    }
65}