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