viz_handlers/
embed.rs

1//! Static files serving and embedding.
2
3use std::{borrow::Cow, marker::PhantomData};
4
5use http_body_util::Full;
6use rust_embed::{EmbeddedFile, RustEmbed};
7use viz_core::{
8    header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
9    Handler, IntoResponse, Method, Request, RequestExt, Response, Result, StatusCode,
10};
11
12/// Serve a single embedded file.
13#[derive(Debug)]
14pub struct File<E>(Cow<'static, str>, PhantomData<E>);
15
16impl<E> Clone for File<E> {
17    fn clone(&self) -> Self {
18        Self(self.0.clone(), PhantomData)
19    }
20}
21
22impl<E> File<E> {
23    /// Serve a new file by the specified path.
24    #[must_use]
25    pub fn new(path: &'static str) -> Self {
26        Self(path.into(), PhantomData)
27    }
28}
29
30#[viz_core::async_trait]
31impl<E> Handler<Request> for File<E>
32where
33    E: RustEmbed + Send + Sync + 'static,
34{
35    type Output = Result<Response>;
36
37    async fn call(&self, req: Request) -> Self::Output {
38        serve::<E>(&self.0, &req)
39    }
40}
41
42/// Serve a embedded directory.
43#[derive(Debug)]
44pub struct Dir<E>(PhantomData<E>);
45
46impl<E> Clone for Dir<E> {
47    fn clone(&self) -> Self {
48        Self(PhantomData)
49    }
50}
51
52impl<E> Default for Dir<E> {
53    fn default() -> Self {
54        Self(PhantomData)
55    }
56}
57
58#[viz_core::async_trait]
59impl<E> Handler<Request> for Dir<E>
60where
61    E: RustEmbed + Send + Sync + 'static,
62{
63    type Output = Result<Response>;
64
65    async fn call(&self, req: Request) -> Self::Output {
66        serve::<E>(
67            req.route_info()
68                .params
69                .first()
70                .map(|(_, v)| v)
71                .map_or("index.html", |p| p),
72            &req,
73        )
74    }
75}
76
77fn serve<E>(path: &str, req: &Request) -> Result<Response>
78where
79    E: RustEmbed + Send + Sync + 'static,
80{
81    if Method::GET != req.method() {
82        Err(StatusCode::METHOD_NOT_ALLOWED.into_error())?;
83    }
84
85    match E::get(path) {
86        Some(EmbeddedFile { data, metadata }) => {
87            let hash = hex::encode(metadata.sha256_hash());
88
89            if req
90                .headers()
91                .get(IF_NONE_MATCH)
92                .is_some_and(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
93            {
94                Err(StatusCode::NOT_MODIFIED.into_error())?;
95            }
96
97            Response::builder()
98                .header(
99                    CONTENT_TYPE,
100                    mime_guess::from_path(path).first_or_octet_stream().as_ref(),
101                )
102                .header(ETAG, hash)
103                .body(Full::from(data).into())
104                .map_err(Into::into)
105        }
106        None => Err(StatusCode::NOT_FOUND.into_error()),
107    }
108}