1use 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#[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 #[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#[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}