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
This commit is contained in:
parent
bbbb9e190f
commit
a43aea3320
2 changed files with 88 additions and 2 deletions
|
|
@ -13,6 +13,69 @@ use std::sync::Arc;
|
||||||
|
|
||||||
/// Rewrite CDN URLs to local paths for HTMX and other vendor libraries
|
/// Rewrite CDN URLs to local paths for HTMX and other vendor libraries
|
||||||
/// This ensures old apps with CDN references still work with local files
|
/// 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<Arc<AppState>>,
|
||||||
|
Path(params): Path<VendorFilePath>,
|
||||||
|
) -> 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 {
|
fn rewrite_cdn_urls(html: &str) -> String {
|
||||||
html
|
html
|
||||||
// HTMX from various CDNs
|
// HTMX from various CDNs
|
||||||
|
|
@ -31,6 +94,8 @@ fn rewrite_cdn_urls(html: &str) -> String {
|
||||||
|
|
||||||
pub fn configure_app_server_routes() -> Router<Arc<AppState>> {
|
pub fn configure_app_server_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
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)
|
// Serve app files: /apps/{app_name}/* (clean URLs)
|
||||||
.route("/apps/:app_name", get(serve_app_index))
|
.route("/apps/:app_name", get(serve_app_index))
|
||||||
.route("/apps/:app_name/", get(serve_app_index))
|
.route("/apps/:app_name/", get(serve_app_index))
|
||||||
|
|
|
||||||
25
src/main.rs
25
src/main.rs
|
|
@ -23,6 +23,27 @@ use std::sync::Arc;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_http::trace::TraceLayer;
|
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::{
|
use botserver::security::{
|
||||||
auth_middleware, create_cors_layer, create_rate_limit_layer, create_security_headers_layer,
|
auth_middleware, create_cors_layer, create_rate_limit_layer, create_security_headers_layer,
|
||||||
request_id_middleware, security_headers_middleware, set_cors_allowed_origins,
|
request_id_middleware, security_headers_middleware, set_cors_allowed_origins,
|
||||||
|
|
@ -314,8 +335,6 @@ async fn run_axum_server(
|
||||||
auth_config.clone(),
|
auth_config.clone(),
|
||||||
auth_middleware,
|
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
|
// Static files fallback for legacy /apps/* paths
|
||||||
.nest_service("/static", ServeDir::new(&site_path))
|
.nest_service("/static", ServeDir::new(&site_path))
|
||||||
// Security middleware stack (order matters - first added is outermost)
|
// Security middleware stack (order matters - first added is outermost)
|
||||||
|
|
@ -730,6 +749,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| std::io::Error::other(format!("Failed to initialize Drive: {}", e)))?;
|
.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(
|
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)))?,
|
pool.get().map_err(|e| std::io::Error::other(format!("Failed to get database connection: {}", e)))?,
|
||||||
redis_client.clone(),
|
redis_client.clone(),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue