diff --git a/libs/api/dist.rs b/libs/api/dist.rs index 826f0c8..27876aa 100644 --- a/libs/api/dist.rs +++ b/libs/api/dist.rs @@ -1,29 +1,69 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{http::header, web, HttpRequest, HttpResponse}; use mime_guess2::MimeGuess; -pub async fn serve_frontend(path: web::Path) -> HttpResponse { - let path = path.into_inner(); - let path = if path.is_empty() || path == "/" { - "index.html" +fn cache_control_header(path: &str) -> &'static str { + if path == "index.html" { + "no-cache, no-store, must-revalidate" + } else if path.ends_with(".js") + || path.ends_with(".css") + || path.ends_with(".woff2") + || path.ends_with(".woff") + || path.ends_with(".ttf") + || path.ends_with(".otf") + || path.ends_with(".png") + || path.ends_with(".jpg") + || path.ends_with(".jpeg") + || path.ends_with(".gif") + || path.ends_with(".svg") + || path.ends_with(".ico") + || path.ends_with(".webp") + || path.ends_with(".avif") + || path.ends_with(".map") + { + "public, max-age=31536000, immutable" } else { - &path - }; - - match frontend::get_frontend_asset(path) { - Some(data) => { - let mime = MimeGuess::from_path(path).first_or_octet_stream(); - HttpResponse::Ok() - .content_type(mime.as_ref()) - .body(data.to_vec()) - } - None => { - // Fallback to index.html for SPA routing - match frontend::get_frontend_asset("index.html") { - Some(data) => HttpResponse::Ok() - .content_type("text/html") - .body(data.to_vec()), - None => HttpResponse::NotFound().finish(), - } - } + "no-cache" + } +} + +pub async fn serve_frontend(req: HttpRequest, path: web::Path) -> HttpResponse { + let path = path.into_inner(); + let path_str = if path.is_empty() || path == "/" { + "index.html" + } else { + path.as_str() + }; + + let cc = cache_control_header(path_str); + + match frontend::get_frontend_asset_with_etag(path_str) { + Some((data, etag)) => { + // Check If-None-Match for conditional request + if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) { + if let Ok(client_etag) = if_none_match.to_str() { + if client_etag == etag { + return HttpResponse::NotModified() + .insert_header(("Cache-Control", cc)) + .insert_header(("ETag", etag)) + .finish(); + } + } + } + + let mime = MimeGuess::from_path(path_str).first_or_octet_stream(); + HttpResponse::Ok() + .content_type(mime.as_ref()) + .insert_header(("Cache-Control", cc)) + .insert_header(("ETag", etag)) + .body(data.to_vec()) + } + None => match frontend::get_frontend_asset_with_etag("index.html") { + Some((data, etag)) => HttpResponse::Ok() + .content_type("text/html") + .insert_header(("Cache-Control", "no-cache, no-store, must-revalidate")) + .insert_header(("ETag", etag)) + .body(data.to_vec()), + None => HttpResponse::NotFound().finish(), + }, } }