From a43aea3320092f77013f478544d0a093a1c1b86d Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Fri, 2 Jan 2026 18:26:34 -0300 Subject: [PATCH] Serve vendor files (htmx) from MinIO instead of local filesystem - Added serve_vendor_file() to serve from {bot}.gblib/vendor/ in MinIO - Added /js/vendor/* route to app_server - Removed local ServeDir for /js/vendor from main.rs - Added ensure_vendor_files_in_minio() to upload htmx.min.js on startup - Uses include_bytes! to embed htmx.min.js in binary --- src/basic/keywords/app_server.rs | 65 ++++++++++++++++++++++++++++++++ src/main.rs | 25 +++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/basic/keywords/app_server.rs b/src/basic/keywords/app_server.rs index 683ce4e4f..e712a3d5b 100644 --- a/src/basic/keywords/app_server.rs +++ b/src/basic/keywords/app_server.rs @@ -13,6 +13,69 @@ use std::sync::Arc; /// Rewrite CDN URLs to local paths for HTMX and other vendor libraries /// This ensures old apps with CDN references still work with local files +#[derive(Debug, serde::Deserialize)] +pub struct VendorFilePath { + pub file_path: String, +} + +pub async fn serve_vendor_file( + State(state): State>, + Path(params): Path, +) -> Response { + let file_path = sanitize_file_path(¶ms.file_path); + + if file_path.is_empty() { + return (StatusCode::BAD_REQUEST, "Invalid path").into_response(); + } + + let bot_name = state.bucket_name + .trim_end_matches(".gbai") + .to_string(); + let sanitized_bot_name = bot_name.to_lowercase().replace(' ', "-").replace('_', "-"); + + let bucket = format!("{}.gbai", sanitized_bot_name); + let key = format!("{}.gblib/vendor/{}", sanitized_bot_name, file_path); + + info!("Serving vendor file from MinIO: bucket={}, key={}", bucket, key); + + if let Some(ref drive) = state.drive { + match drive + .get_object() + .bucket(&bucket) + .key(&key) + .send() + .await + { + Ok(response) => { + match response.body.collect().await { + Ok(body) => { + let content = body.into_bytes(); + let content_type = get_content_type(&file_path); + + return Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type) + .header(header::CACHE_CONTROL, "public, max-age=86400") + .body(Body::from(content.to_vec())) + .unwrap_or_else(|_| { + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to build response") + .into_response() + }); + } + Err(e) => { + error!("Failed to read MinIO response body: {}", e); + } + } + } + Err(e) => { + warn!("MinIO get_object failed for {}/{}: {}", bucket, key, e); + } + } + } + + (StatusCode::NOT_FOUND, "Vendor file not found").into_response() +} + fn rewrite_cdn_urls(html: &str) -> String { html // HTMX from various CDNs @@ -31,6 +94,8 @@ fn rewrite_cdn_urls(html: &str) -> String { pub fn configure_app_server_routes() -> Router> { Router::new() + // Serve shared vendor files from MinIO: /js/vendor/* + .route("/js/vendor/*file_path", get(serve_vendor_file)) // Serve app files: /apps/{app_name}/* (clean URLs) .route("/apps/:app_name", get(serve_app_index)) .route("/apps/:app_name/", get(serve_app_index)) diff --git a/src/main.rs b/src/main.rs index 47c232c61..f0c960518 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,27 @@ use std::sync::Arc; use tower_http::services::ServeDir; use tower_http::trace::TraceLayer; +async fn ensure_vendor_files_in_minio(drive: &aws_sdk_s3::Client) { + use aws_sdk_s3::primitives::ByteStream; + + let htmx_content = include_bytes!("../../botserver-stack/static/js/vendor/htmx.min.js"); + let bucket = "default.gbai"; + let key = "default.gblib/vendor/htmx.min.js"; + + match drive + .put_object() + .bucket(bucket) + .key(key) + .body(ByteStream::from_static(htmx_content)) + .content_type("application/javascript") + .send() + .await + { + Ok(_) => info!("Uploaded vendor file to MinIO: s3://{}/{}", bucket, key), + Err(e) => warn!("Failed to upload vendor file to MinIO: {}", e), + } +} + use botserver::security::{ auth_middleware, create_cors_layer, create_rate_limit_layer, create_security_headers_layer, request_id_middleware, security_headers_middleware, set_cors_allowed_origins, @@ -314,8 +335,6 @@ async fn run_axum_server( auth_config.clone(), auth_middleware, )) - // Vendor JS files (htmx, etc.) served locally - no CDN - .nest_service("/js/vendor", ServeDir::new("./botserver-stack/static/js/vendor")) // Static files fallback for legacy /apps/* paths .nest_service("/static", ServeDir::new(&site_path)) // Security middleware stack (order matters - first added is outermost) @@ -730,6 +749,8 @@ async fn main() -> std::io::Result<()> { .await .map_err(|e| std::io::Error::other(format!("Failed to initialize Drive: {}", e)))?; + ensure_vendor_files_in_minio(&drive).await; + let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( pool.get().map_err(|e| std::io::Error::other(format!("Failed to get database connection: {}", e)))?, redis_client.clone(),