vite_static_actix_web/
service.rs1use 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 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
52pub 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
98impl 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}