actix_embed/
service.rs

1use std::fmt::{Debug, Formatter};
2use std::marker::PhantomData;
3use std::rc::Rc;
4
5use actix_web::body::BoxBody;
6use actix_web::dev::{
7    AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
8    ServiceResponse,
9};
10use actix_web::http::{header, Method};
11use actix_web::HttpResponse;
12use futures_core::future::LocalBoxFuture;
13use mime_guess::MimeGuess;
14
15use crate::fallback_handler::{DefaultFallbackHandler, FallbackHandler};
16
17/// Wrapper of rust_embed for actix.
18///
19/// `Embed` service must be registered with `App::service()` method.
20///
21/// rust_embed documentation: https://docs.rs/rust-embed/
22///
23/// # Examples
24/// ```
25/// use actix_web::App;
26/// use actix_embed::Embed;
27/// use rust_embed::RustEmbed;
28///
29/// #[derive(RustEmbed)]
30/// #[folder = "testdata/"]
31/// struct Assets;
32///
33/// let app = App::new()
34///     .service(Embed::new("/static", &Assets));
35/// ```
36pub struct Embed<E, F>
37where
38    E: 'static + rust_embed::RustEmbed,
39    F: FallbackHandler,
40{
41    mount_path: String,
42    index_file_path: Option<String>,
43    strict_slash: bool,
44    fallback_handler: F,
45    _f: PhantomData<E>,
46}
47
48impl<E, F> Debug for Embed<E, F>
49where
50    E: 'static + rust_embed::RustEmbed,
51    F: FallbackHandler,
52{
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        f.write_str("Embed")
55    }
56}
57
58impl<E> Embed<E, DefaultFallbackHandler>
59where
60    E: 'static + rust_embed::RustEmbed,
61{
62    /// Create new [Embed] instance.
63    ///
64    /// # Arguments
65    /// The first argument (`mount_path`) is the root URL at which the embed files are served.
66    /// For example, `/assets` will serve files at `example.com/assets/...`.
67    ///
68    /// The second argument (`assets`) is the instance implements [rust_embed::RustEmbed].
69    /// For more information, see rust_embed documentation: https://docs.rs/rust-embed/
70    ///
71    /// # Notes
72    /// If the mount path is set as the root path `/`, services registered after this one will
73    /// be inaccessible. Register more specific handlers and services before it.
74    #[allow(unused_variables)]
75    pub fn new<P: AsRef<str>>(mount_path: P, assets: &E) -> Self {
76        Embed {
77            mount_path: mount_path.as_ref().trim_end_matches('/').to_owned(),
78            index_file_path: None,
79            strict_slash: false,
80            fallback_handler: DefaultFallbackHandler,
81            _f: Default::default(),
82        }
83    }
84}
85
86impl<E, F> Embed<E, F>
87where
88    E: 'static + rust_embed::RustEmbed,
89    F: FallbackHandler,
90{
91    /// Set whether to ignore the trailing slash of the requested path.
92    ///
93    /// Defaults to `false`.
94    ///
95    /// If it's set to true, then file '/dir/file' cannot be accessed by request path '/dir/file/'.
96    pub fn strict_slash(mut self, strict_slash: bool) -> Self {
97        self.strict_slash = strict_slash;
98        self
99    }
100
101    /// Set the path of the index file.
102    ///
103    /// By default there is no index file.
104    ///
105    /// The index file is treated as the default file returned when a request
106    /// visit the root directory.
107    pub fn index_file<P: AsRef<str>>(mut self, path: P) -> Self {
108        self.index_file_path = Some(
109            path.as_ref()
110                .trim_end_matches('/')
111                .trim_start_matches('/')
112                .to_string(),
113        );
114        self
115    }
116
117    /// Sets fallback handler which is used when no matched file could be found.
118    ///
119    /// The default fallback handler returns 404 responses.
120    ///
121    /// # Examples
122    /// ```
123    /// use actix_embed::Embed;
124    /// use actix_web::HttpResponse;
125    /// use rust_embed::RustEmbed;
126    ///
127    /// #[derive(RustEmbed)]
128    /// #[folder = "testdata/"]
129    /// struct Assets;
130    ///
131    /// # fn run() {
132    /// let embed = Embed::new("/static", &Assets)
133    ///     .index_file("index.html")
134    ///     .fallback_handler(|_: &_| HttpResponse::BadRequest().body("not found"));
135    /// # }
136    /// ```
137    ///
138    /// # Note
139    /// It is necessary to add type annotation for the closure parameters like `|_: &_| ...`.
140    ///
141    /// See https://github.com/rust-lang/rust/issues/41078
142    pub fn fallback_handler<NF>(self, handler: NF) -> Embed<E, NF>
143    where
144        NF: FallbackHandler,
145    {
146        Embed {
147            mount_path: self.mount_path,
148            index_file_path: self.index_file_path,
149            strict_slash: self.strict_slash,
150            fallback_handler: handler,
151            _f: Default::default(),
152        }
153    }
154}
155
156impl<E, F> HttpServiceFactory for Embed<E, F>
157where
158    E: 'static + rust_embed::RustEmbed,
159    F: FallbackHandler,
160{
161    fn register(self, config: &mut AppService) {
162        let resource_def = if config.is_root() {
163            ResourceDef::root_prefix(&self.mount_path)
164        } else {
165            ResourceDef::prefix(&self.mount_path)
166        };
167        config.register_service(resource_def, None, self, None)
168    }
169}
170
171impl<E, F> ServiceFactory<ServiceRequest> for Embed<E, F>
172where
173    E: 'static + rust_embed::RustEmbed,
174    F: FallbackHandler,
175{
176    type Response = ServiceResponse;
177    type Error = actix_web::Error;
178    type Config = ();
179    type Service = EmbedService<E, F>;
180    type InitError = ();
181    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
182
183    fn new_service(&self, _: ()) -> Self::Future {
184        let strict_slash = self.strict_slash;
185        let fallback_handler = self.fallback_handler.clone();
186        let index_file_path = self.index_file_path.clone();
187
188        Box::pin(async move {
189            Ok(EmbedService::new(EmbedServiceInner {
190                strict_slash,
191                index_file_path,
192                fallback_handler,
193            }))
194        })
195    }
196}
197
198#[derive(Clone)]
199pub struct EmbedService<E, F>
200where
201    E: 'static + rust_embed::RustEmbed,
202    F: FallbackHandler,
203{
204    inner: Rc<EmbedServiceInner<F>>,
205    _e: PhantomData<E>,
206}
207
208impl<E, F> Debug for EmbedService<E, F>
209where
210    E: 'static + rust_embed::RustEmbed,
211    F: FallbackHandler,
212{
213    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
214        f.write_str("EmbedService")
215    }
216}
217
218impl<E, F> EmbedService<E, F>
219where
220    E: 'static + rust_embed::RustEmbed,
221    F: FallbackHandler,
222{
223    pub(crate) fn new(inner: EmbedServiceInner<F>) -> Self {
224        Self {
225            inner: Rc::new(inner),
226            _e: Default::default(),
227        }
228    }
229}
230
231pub(crate) struct EmbedServiceInner<F>
232where
233    F: FallbackHandler,
234{
235    strict_slash: bool,
236    index_file_path: Option<String>,
237    fallback_handler: F,
238}
239
240impl<E, F> Service<ServiceRequest> for EmbedService<E, F>
241where
242    E: 'static + rust_embed::RustEmbed,
243    F: FallbackHandler,
244{
245    type Response = ServiceResponse<BoxBody>;
246    type Error = actix_web::Error;
247    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
248
249    actix_web::dev::always_ready!();
250
251    fn call(&self, req: ServiceRequest) -> Self::Future {
252        let this = self.inner.clone();
253
254        Box::pin(async move {
255            if Method::GET.ne(req.method()) {
256                return Ok(req.into_response(HttpResponse::MethodNotAllowed()));
257            }
258            let mut path = req.path();
259            path = path.trim_start_matches('/');
260            if !this.strict_slash {
261                path = path.trim_end_matches('/');
262            }
263            if path.is_empty() {
264                path = this.index_file_path.as_deref().unwrap_or("")
265            }
266
267            match E::get(path) {
268                Some(f) => {
269                    let hash = hex::encode(f.metadata.sha256_hash());
270
271                    if req
272                        .headers()
273                        .get(header::IF_NONE_MATCH)
274                        .map(|v| v.to_str().unwrap_or("0").eq(&hash))
275                        .unwrap_or(false)
276                    {
277                        return Ok(req.into_response(HttpResponse::NotModified()));
278                    }
279
280                    let mime = MimeGuess::from_path(path).first_or_octet_stream();
281                    let data = f.data.into_owned();
282
283                    Ok(req.into_response(
284                        HttpResponse::Ok()
285                            .content_type(mime.as_ref())
286                            .insert_header((header::ETAG, hash))
287                            .body(data),
288                    ))
289                }
290                None => {
291                    let (req, _) = req.into_parts();
292                    let resp = this.fallback_handler.execute(&req);
293                    Ok(ServiceResponse::new(req, resp))
294                }
295            }
296        })
297    }
298}