vite-static-actix-web 1.1.0

Actix Web integration support for `vite-static`
Documentation
use std::ops::Deref;

use actix_web::{
    Error, HttpMessage as _, HttpRequest, HttpResponse,
    dev::{
        AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
        ServiceResponse, always_ready,
    },
    http::{
        Method, StatusCode,
        header::{CONTENT_TYPE, ETag, EntityTag, IfMatch, IfNoneMatch},
    },
};
use futures_util::future::{FutureExt, LocalBoxFuture, Ready, ok};

use vite_static_shared::ManifestChunk;

use crate::ActixFiles;

impl HttpServiceFactory for ActixFiles {
    fn register(self, config: &mut AppService) {
        let base = self.manifest.base();
        let path = base.trim_start_matches('/');

        let rdef = if config.is_root() {
            ResourceDef::root_prefix(path)
        } else {
            ResourceDef::prefix(path)
        };

        // TODO: guards
        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()
    }
}

/// Internal implementation of [`ActixFiles`] service.
///
/// Use [`ActixFiles`] instead!
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: &ManifestChunk<'_>) -> HttpResponse {
        let mut response = HttpResponse::Ok();
        response.insert_header((CONTENT_TYPE, chunk.mime_type.as_ref()));

        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());

        match *req.method() {
            Method::HEAD => response.finish(),
            _ => response.body(chunk.contents.to_vec()),
        }
    }
}

// TODO(#11): add more features (like `Content-Range`, default handler, etc.)
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 | Method::HEAD => (),
            _ => {
                return ok(ServiceResponse::new(
                    req.into_parts().0,
                    HttpResponse::MethodNotAllowed().body("Only GET and HEAD 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))
    }
}