salvo_serve_static/
embed.rs1use std::borrow::Cow;
2use std::marker::PhantomData;
3
4use rust_embed::{EmbeddedFile, Metadata, RustEmbed};
5use salvo_core::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH};
6use salvo_core::http::{HeaderValue, Mime, Request, Response, StatusCode};
7use salvo_core::{async_trait, Depot, FlowCtrl, IntoVecString};
8use salvo_core::handler::{ Handler};
9
10use super::{decode_url_path_safely, format_url_path_safely, join_path, redirect_to_dir_url};
11
12#[non_exhaustive]
17#[derive(Default)]
18pub struct StaticEmbed<T> {
19 _assets: PhantomData<T>,
20 pub defaults: Vec<String>,
22 pub fallback: Option<String>,
24}
25
26#[inline]
28pub fn static_embed<T: RustEmbed>() -> StaticEmbed<T> {
29 StaticEmbed {
30 _assets: PhantomData,
31 defaults: vec![],
32 fallback: None,
33 }
34}
35
36#[inline]
38pub fn render_embedded_file(file: EmbeddedFile, req: &Request, res: &mut Response, mime: Option<Mime>) {
39 let EmbeddedFile { data, metadata, .. } = file;
40 render_embedded_data(data, &metadata, req, res, mime);
41}
42
43fn render_embedded_data(
44 data: Cow<'static, [u8]>,
45 metadata: &Metadata,
46 req: &Request,
47 res: &mut Response,
48 mime: Option<Mime>,
49) {
50 let mime = mime.unwrap_or_else(|| mime_infer::from_path(req.uri().path()).first_or_octet_stream());
51 res.headers_mut().insert(
52 CONTENT_TYPE,
53 mime.as_ref()
54 .parse()
55 .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
56 );
57
58 let hash = hex::encode(metadata.sha256_hash());
59 if req
61 .headers()
62 .get(IF_NONE_MATCH)
63 .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash))
64 .unwrap_or(false)
65 {
66 res.status_code(StatusCode::NOT_MODIFIED);
67 return;
68 }
69
70 if let Ok(hash) = hash.parse() {
72 res.headers_mut().insert(ETAG, hash);
73 } else {
74 tracing::error!("Failed to parse etag hash: {}", hash);
75 }
76
77 match data {
78 Cow::Borrowed(data) => {
79 let _ = res.write_body(data);
80 }
81 Cow::Owned(data) => {
82 let _ = res.write_body(data);
83 }
84 }
85}
86
87impl<T> StaticEmbed<T>
88where
89 T: RustEmbed + Send + Sync + 'static,
90{
91 #[inline]
93 pub fn new() -> Self {
94 Self {
95 _assets: PhantomData,
96 defaults: vec![],
97 fallback: None,
98 }
99 }
100
101 #[inline]
103 pub fn defaults(mut self, defaults: impl IntoVecString) -> Self {
104 self.defaults = defaults.into_vec_string();
105 self
106 }
107
108 #[inline]
110 pub fn fallback(mut self, fallback: impl Into<String>) -> Self {
111 self.fallback = Some(fallback.into());
112 self
113 }
114}
115#[async_trait]
116impl<T> Handler for StaticEmbed<T>
117where
118 T: RustEmbed + Send + Sync + 'static,
119{
120 async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
121 let req_path = if let Some(rest) = req.params().tail() {
122 rest
123 } else {
124 &*decode_url_path_safely(req.uri().path())
125 };
126 let req_path = format_url_path_safely(req_path);
127 let mut key_path = Cow::Borrowed(&*req_path);
128 let mut embedded_file = T::get(req_path.as_str());
129 if embedded_file.is_none() {
130 for ifile in &self.defaults {
131 let ipath = join_path!(&req_path, ifile);
132 if let Some(file) = T::get(&ipath) {
133 embedded_file = Some(file);
134 key_path = Cow::from(ipath);
135 break;
136 }
137 }
138 if embedded_file.is_some() && !req_path.ends_with('/') && !req_path.is_empty() {
139 redirect_to_dir_url(req.uri(), res);
140 return;
141 }
142 }
143 if embedded_file.is_none() {
144 let fallback = self.fallback.as_deref().unwrap_or_default();
145 if !fallback.is_empty() {
146 if let Some(file) = T::get(fallback) {
147 embedded_file = Some(file);
148 key_path = Cow::from(fallback);
149 }
150 }
151 }
152
153 match embedded_file {
154 Some(file) => {
155 let mime = mime_infer::from_path(&*key_path).first_or_octet_stream();
156 render_embedded_file(file, req, res, Some(mime));
157 }
158 None => {
159 res.status_code(StatusCode::NOT_FOUND);
160 }
161 }
162 }
163}
164
165pub struct EmbeddedFileHandler(pub EmbeddedFile);
167
168#[async_trait]
169impl Handler for EmbeddedFileHandler {
170 #[inline]
171 async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
172 render_embedded_data(self.0.data.clone(), &self.0.metadata, req, res, None);
173 }
174}
175
176pub trait EmbeddedFileExt {
178 fn render(self, req: &Request, res: &mut Response);
180 fn into_handler(self) -> EmbeddedFileHandler;
182}
183
184impl EmbeddedFileExt for EmbeddedFile {
185 #[inline]
186 fn render(self, req: &Request, res: &mut Response) {
187 render_embedded_file(self, req, res, None);
188 }
189 #[inline]
190 fn into_handler(self) -> EmbeddedFileHandler {
191 EmbeddedFileHandler(self)
192 }
193}