vite_static_actix_web/
lib.rs1#![warn(clippy::pedantic)]
2
3use std::{ops::Deref, rc::Rc};
4use vite_static_shared::{DynManifest, ManifestChunk};
5
6#[allow(unused_imports)]
7use vite_static_shared::Manifest;
8
9use actix_web::{
10 Error, HttpMessage as _, HttpRequest, HttpResponse,
11 dev::{
12 AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
13 ServiceResponse, always_ready,
14 },
15 http::{
16 Method, StatusCode,
17 header::{
18 CONTENT_TYPE, CacheControl, CacheDirective, ETag, EntityTag, IfMatch, IfNoneMatch,
19 },
20 },
21};
22use futures_util::future::{FutureExt, LocalBoxFuture, Ready, ok};
23
24#[derive(Clone)]
43pub struct ActixFiles {
44 manifest: Rc<DynManifest<'static>>,
45 cache_control: CacheControl,
46}
47
48impl ActixFiles {
49 #[must_use]
57 pub fn new(manifest: DynManifest<'static>) -> Self {
58 Self {
59 manifest: Rc::new(manifest),
60 cache_control: CacheControl(vec![CacheDirective::MaxAge(604_800)]),
61 }
62 }
63
64 #[must_use]
73 pub fn cache_control(mut self, value: CacheControl) -> Self {
74 self.cache_control = value;
75 self
76 }
77}
78
79impl HttpServiceFactory for ActixFiles {
80 fn register(self, config: &mut AppService) {
81 let base = self.manifest.base();
82 let path = base.trim_start_matches('/');
83
84 let rdef = if config.is_root() {
85 ResourceDef::root_prefix(path)
86 } else {
87 ResourceDef::prefix(path)
88 };
89
90 config.register_service(rdef, None, self, None);
92 }
93}
94
95impl ServiceFactory<ServiceRequest> for ActixFiles {
96 type Response = ServiceResponse;
97 type Error = Error;
98 type Config = ();
99 type Service = ActixFilesService;
100 type InitError = ();
101 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
102
103 fn new_service(&self, _cfg: Self::Config) -> Self::Future {
104 ok(ActixFilesService {
105 inner: self.clone(),
106 })
107 .boxed_local()
108 }
109}
110
111pub struct ActixFilesService {
115 inner: ActixFiles,
116}
117
118impl Deref for ActixFilesService {
119 type Target = ActixFiles;
120
121 fn deref(&self) -> &Self::Target {
122 &self.inner
123 }
124}
125
126impl ActixFilesService {
127 fn respond_to(&self, req: &HttpRequest, chunk: &ManifestChunk<'_>) -> HttpResponse {
128 let mut response = HttpResponse::Ok();
129 response.insert_header((CONTENT_TYPE, chunk.mime_type.as_ref()));
130
131 let etag = EntityTag::new_strong(chunk.hash.to_string());
132 response.insert_header(ETag(etag.clone()));
133
134 let precondition_failed = match req.get_header::<IfMatch>() {
135 None | Some(IfMatch::Any) => false,
136 Some(IfMatch::Items(items)) => items.iter().all(|item| item.strong_ne(&etag)),
137 };
138 if precondition_failed {
139 return response.status(StatusCode::PRECONDITION_FAILED).finish();
140 }
141
142 let not_modified = match req.get_header::<IfNoneMatch>() {
143 None => false,
144 Some(IfNoneMatch::Any) => true,
145 Some(IfNoneMatch::Items(items)) => items.iter().all(|item| item.weak_ne(&etag)),
146 };
147 if not_modified {
148 return response.status(StatusCode::NOT_MODIFIED).finish();
149 }
150
151 response.insert_header(self.cache_control.clone());
152 response.body(chunk.contents.to_vec())
153 }
154}
155
156impl Service<ServiceRequest> for ActixFilesService {
157 type Response = ServiceResponse;
158 type Error = Error;
159 type Future = Ready<Result<Self::Response, Self::Error>>;
160
161 always_ready!();
162
163 fn call(&self, req: ServiceRequest) -> Self::Future {
164 match *req.method() {
165 Method::GET => (),
166 _ => {
167 return ok(ServiceResponse::new(
168 req.into_parts().0,
169 HttpResponse::MethodNotAllowed().body("Only GET requests are allowed"),
170 ));
171 }
172 }
173
174 let (req, _) = req.into_parts();
175 let requested_path = req.match_info().unprocessed().trim_start_matches('/');
176
177 let response = match self.manifest.chunk(requested_path) {
178 Some(chunk) => self.respond_to(&req, &chunk),
179 None => HttpResponse::NotFound().body("Not found"),
180 };
181
182 ok(ServiceResponse::new(req, response))
183 }
184}