Skip to main content

vite_static_actix_web/
service.rs

1use std::ops::Deref;
2
3use actix_web::{
4    Error, HttpMessage as _, HttpRequest, HttpResponse,
5    dev::{
6        AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
7        ServiceResponse, always_ready,
8    },
9    http::{
10        Method, StatusCode,
11        header::{CONTENT_TYPE, ETag, EntityTag, IfMatch, IfNoneMatch},
12    },
13};
14use futures_util::future::{FutureExt, LocalBoxFuture, Ready, ok};
15
16use vite_static_shared::ManifestChunk;
17
18use crate::ActixFiles;
19
20impl HttpServiceFactory for ActixFiles {
21    fn register(self, config: &mut AppService) {
22        let base = self.manifest.base();
23        let path = base.trim_start_matches('/');
24
25        let rdef = if config.is_root() {
26            ResourceDef::root_prefix(path)
27        } else {
28            ResourceDef::prefix(path)
29        };
30
31        // TODO: guards
32        config.register_service(rdef, None, self, None);
33    }
34}
35
36impl ServiceFactory<ServiceRequest> for ActixFiles {
37    type Response = ServiceResponse;
38    type Error = Error;
39    type Config = ();
40    type Service = ActixFilesService;
41    type InitError = ();
42    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
43
44    fn new_service(&self, _cfg: Self::Config) -> Self::Future {
45        ok(ActixFilesService {
46            inner: self.clone(),
47        })
48        .boxed_local()
49    }
50}
51
52/// Internal implementation of [`ActixFiles`] service.
53///
54/// Use [`ActixFiles`] instead!
55pub struct ActixFilesService {
56    inner: ActixFiles,
57}
58
59impl Deref for ActixFilesService {
60    type Target = ActixFiles;
61
62    fn deref(&self) -> &Self::Target {
63        &self.inner
64    }
65}
66
67impl ActixFilesService {
68    fn respond_to(&self, req: &HttpRequest, chunk: &ManifestChunk<'_>) -> HttpResponse {
69        let mut response = HttpResponse::Ok();
70        response.insert_header((CONTENT_TYPE, chunk.mime_type.as_ref()));
71
72        let etag = EntityTag::new_strong(chunk.hash.to_string());
73        response.insert_header(ETag(etag.clone()));
74
75        let precondition_failed = match req.get_header::<IfMatch>() {
76            None | Some(IfMatch::Any) => false,
77            Some(IfMatch::Items(items)) => items.iter().any(|item| item.strong_ne(&etag)),
78        };
79        if precondition_failed {
80            return response.status(StatusCode::PRECONDITION_FAILED).finish();
81        }
82
83        let not_modified = match req.get_header::<IfNoneMatch>() {
84            None => false,
85            Some(IfNoneMatch::Any) => true,
86            Some(IfNoneMatch::Items(items)) => !items.iter().any(|item| item.weak_eq(&etag)),
87        };
88        if not_modified {
89            return response.status(StatusCode::NOT_MODIFIED).finish();
90        }
91
92        response.insert_header(self.cache_control.clone());
93
94        response.body(chunk.contents.to_vec())
95    }
96}
97
98// TODO(#11): add more features (like `Content-Range`, default handler, etc.)
99impl Service<ServiceRequest> for ActixFilesService {
100    type Response = ServiceResponse;
101    type Error = Error;
102    type Future = Ready<Result<Self::Response, Self::Error>>;
103
104    always_ready!();
105
106    fn call(&self, req: ServiceRequest) -> Self::Future {
107        match *req.method() {
108            Method::GET | Method::HEAD => (),
109            _ => {
110                return ok(ServiceResponse::new(
111                    req.into_parts().0,
112                    HttpResponse::MethodNotAllowed().body("Only GET and HEAD requests are allowed"),
113                ));
114            }
115        }
116
117        let (req, _) = req.into_parts();
118        let requested_path = req.match_info().unprocessed().trim_start_matches('/');
119
120        let response = match self.manifest.chunk(requested_path) {
121            Some(chunk) => self.respond_to(&req, &chunk),
122            None => HttpResponse::NotFound().body("Not found"),
123        };
124        ok(ServiceResponse::new(req, response))
125    }
126}