salvo_serve_static/
embed.rs

1use 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/// Handler that serves embedded files using `rust-embed`.
13///
14/// This handler allows serving files embedded in the application binary,
15/// which is useful for distributing a self-contained executable.
16#[non_exhaustive]
17#[derive(Default)]
18pub struct StaticEmbed<T> {
19    _assets: PhantomData<T>,
20    /// Default file names list (e.g., "index.html")
21    pub defaults: Vec<String>,
22    /// Fallback file name used when the requested file isn't found
23    pub fallback: Option<String>,
24}
25
26/// Create a new `StaticEmbed` handler for the given embedded asset type.
27#[inline]
28pub fn static_embed<T: RustEmbed>() -> StaticEmbed<T> {
29    StaticEmbed {
30        _assets: PhantomData,
31        defaults: vec![],
32        fallback: None,
33    }
34}
35
36/// Render an [`EmbeddedFile`] to the [`Response`].
37#[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 etag is matched, return 304
60    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    // otherwise, return 200 with etag hash
71    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    /// Create a new `StaticEmbed`.
92    #[inline]
93    pub fn new() -> Self {
94        Self {
95            _assets: PhantomData,
96            defaults: vec![],
97            fallback: None,
98        }
99    }
100
101    /// Create a new `StaticEmbed` with defaults.
102    #[inline]
103    pub fn defaults(mut self, defaults: impl IntoVecString) -> Self {
104        self.defaults = defaults.into_vec_string();
105        self
106    }
107
108    /// Create a new `StaticEmbed` with fallback.
109    #[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
165/// Handler for [`EmbeddedFile`].
166pub 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
176/// Extension trait for [`EmbeddedFile`].
177pub trait EmbeddedFileExt {
178    /// Render the embedded file.
179    fn render(self, req: &Request, res: &mut Response);
180    /// Create a handler for the embedded file.
181    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}