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
17pub 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 #[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 pub fn strict_slash(mut self, strict_slash: bool) -> Self {
97 self.strict_slash = strict_slash;
98 self
99 }
100
101 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 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}