use crate::hook::HookService; use actix_web::{App, HttpServer, HttpResponse, web}; use config::AppConfig; use db::cache::AppCache; use db::database::AppDatabase; use sea_orm::ConnectionTrait; use std::sync::Arc; pub mod auth; pub mod handler; pub mod lfs; pub mod lfs_routes; pub mod rate_limit; pub mod routes; pub mod utils; #[derive(Clone)] pub struct HttpAppState { pub db: AppDatabase, pub cache: AppCache, pub sync: crate::ssh::ReceiveSyncService, pub rate_limiter: Arc, pub config: AppConfig, } async fn robots(state: web::Data) -> HttpResponse { let sitemap_url = state .config .git_http_domain() .map(|d| format!("{}/sitemap.xml", d.trim_end_matches('/'))) .unwrap_or_default(); let body = if sitemap_url.is_empty() { "User-agent: *\nDisallow: /\n".to_string() } else { format!("User-agent: *\nDisallow: /\n\nSitemap: {sitemap_url}\n") }; HttpResponse::Ok() .content_type("text/plain; charset=utf-8") .body(body) } async fn health(state: web::Data) -> HttpResponse { let db_ok = state .db .query_one_raw(sea_orm::Statement::from_string( sea_orm::DbBackend::Postgres, "SELECT 1", )) .await .is_ok(); let cache_ok = state.cache.conn().await.is_ok(); if db_ok && cache_ok { HttpResponse::Ok().json(serde_json::json!({ "status": "ok", "db": "ok", "cache": "ok", })) } else { HttpResponse::ServiceUnavailable().json(serde_json::json!({ "status": "unhealthy", "db": if db_ok { "ok" } else { "error" }, "cache": if cache_ok { "ok" } else { "error" }, })) } } pub fn git_http_cfg(cfg: &mut web::ServiceConfig) { cfg.route("/robots.txt", web::get().to(robots)) .route("/health", web::get().to(health)) .route( "/{namespace}/{repo_name}.git/info/refs", web::get().to(routes::info_refs), ) .route( "/{namespace}/{repo_name}.git/git-upload-pack", web::post().to(routes::upload_pack), ) .route( "/{namespace}/{repo_name}.git/git-receive-pack", web::post().to(routes::receive_pack), ) .route( "/{namespace}/{repo_name}.git/info/lfs/objects/batch", web::post().to(lfs_routes::lfs_batch), ) .route( "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}", web::put().to(lfs_routes::lfs_upload), ) .route( "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}", web::get().to(lfs_routes::lfs_download), ) .route( "/{namespace}/{repo_name}.git/info/lfs/locks", web::post().to(lfs_routes::lfs_lock_create), ) .route( "/{namespace}/{repo_name}.git/info/lfs/locks", web::get().to(lfs_routes::lfs_lock_list), ) .route( "/{namespace}/{repo_name}.git/info/lfs/locks/{id}", web::get().to(lfs_routes::lfs_lock_get), ) .route( "/{namespace}/{repo_name}.git/info/lfs/locks/{id}", web::delete().to(lfs_routes::lfs_lock_delete), ); } pub async fn run_http(config: AppConfig) -> anyhow::Result<()> { let (db, app_cache) = tokio::join!(AppDatabase::init(&config), AppCache::init(&config),); let db = db?; let app_cache = app_cache?; let redis_pool = app_cache.redis_pool().clone(); let hook = HookService::new( db.clone(), app_cache.clone(), redis_pool.clone(), config.clone(), ); let _worker_cancel = hook.start_worker(); tracing::info!("hook worker started"); let sync = crate::ssh::ReceiveSyncService::new(redis_pool.clone()); let rate_limiter = Arc::new(rate_limit::RateLimiter::new( rate_limit::RateLimitConfig::default(), )); let _cleanup = rate_limiter.clone().start_cleanup(); let state = HttpAppState { db: db.clone(), cache: app_cache.clone(), sync, rate_limiter, config: config.clone(), }; tracing::info!("Starting git HTTP server on 0.0.0.0:8021"); let server = HttpServer::new(move || { App::new() .app_data(web::Data::new(state.clone())) .configure(git_http_cfg) }) .bind("0.0.0.0:8021")? .run(); // Await the server. Actix-web handles Ctrl+C gracefully by default: // workers finish in-flight requests then exit (graceful shutdown). let result = server.await; if let Err(e) = result { tracing::error!("HTTP server error: {}", e); } tracing::info!("Git HTTP server stopped"); Ok(()) }