#![warn(clippy::pedantic)]
use std::{ops::Deref, rc::Rc};
use vite_static_shared::{Manifest, StaticManifestChunk};
use actix_web::{
Error, HttpMessage as _, HttpRequest, HttpResponse,
dev::{
AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
ServiceResponse, always_ready,
},
http::{
Method, StatusCode,
header::{
CONTENT_TYPE, CacheControl, CacheDirective, ETag, EntityTag, IfMatch, IfNoneMatch,
},
},
};
use futures_util::future::{FutureExt, LocalBoxFuture, Ready, ok};
#[derive(Clone)]
pub struct ActixFiles {
path: String,
manifest: Rc<Box<dyn Manifest<'static>>>,
cache_control: CacheControl,
}
impl ActixFiles {
#[must_use]
pub fn new(path: &str, manifest: Box<dyn Manifest<'static>>) -> Self {
Self {
path: path.to_string(),
manifest: Rc::new(manifest),
cache_control: CacheControl(vec![CacheDirective::MaxAge(604800)]),
}
}
#[must_use]
pub fn cache_control(mut self, value: CacheControl) -> Self {
self.cache_control = value;
self
}
}
impl HttpServiceFactory for ActixFiles {
fn register(self, config: &mut AppService) {
let path = self.path.trim_start_matches('/');
let rdef = if config.is_root() {
ResourceDef::root_prefix(path)
} else {
ResourceDef::prefix(path)
};
config.register_service(rdef, None, self, None);
}
}
impl ServiceFactory<ServiceRequest> for ActixFiles {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = ActixFilesService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _cfg: Self::Config) -> Self::Future {
ok(ActixFilesService {
inner: self.clone(),
})
.boxed_local()
}
}
pub struct ActixFilesService {
inner: ActixFiles,
}
impl Deref for ActixFilesService {
type Target = ActixFiles;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl ActixFilesService {
fn respond_to(&self, req: &HttpRequest, chunk: &'static StaticManifestChunk) -> HttpResponse {
let mut response = HttpResponse::Ok();
response.insert_header((CONTENT_TYPE, chunk.mime_type));
let etag = EntityTag::new_strong(chunk.hash.to_string());
response.insert_header(ETag(etag.clone()));
let precondition_failed = match req.get_header::<IfMatch>() {
None | Some(IfMatch::Any) => false,
Some(IfMatch::Items(items)) => items.iter().all(|item| item.strong_ne(&etag)),
};
if precondition_failed {
return response.status(StatusCode::PRECONDITION_FAILED).finish();
}
let not_modified = match req.get_header::<IfNoneMatch>() {
None => false,
Some(IfNoneMatch::Any) => true,
Some(IfNoneMatch::Items(items)) => items.iter().all(|item| item.weak_ne(&etag)),
};
if not_modified {
return response.status(StatusCode::NOT_MODIFIED).finish();
}
response.insert_header(self.cache_control.clone());
response.body(chunk.contents)
}
}
impl Service<ServiceRequest> for ActixFilesService {
type Response = ServiceResponse;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
match *req.method() {
Method::GET => (),
_ => {
return ok(ServiceResponse::new(
req.into_parts().0,
HttpResponse::MethodNotAllowed().body("Only GET requests are allowed"),
));
}
}
let (req, _) = req.into_parts();
let requested_path = req.match_info().unprocessed().trim_start_matches('/');
let response = match self.manifest.chunk(requested_path) {
Some(chunk) => self.respond_to(&req, chunk),
None => HttpResponse::NotFound().body("Not found"),
};
ok(ServiceResponse::new(req, response))
}
}