use std::{path::PathBuf, time::Duration}; use actix_web::{Error, HttpRequest, HttpResponse, web}; use tokio::time::timeout; use crate::{ http::{ HttpAppState, auth::authorize_repo_access, handler::GitHttpHandler, utils::get_repo_model, }, sync::{ RepoReceiveSyncTask, push_queue::{ PushQueueEvent, PushQueueWaitError, wait_for_push_queue_slot, }, }, }; #[tracing::instrument(skip(req, state), fields(namespace = %path.0, repo = %path.1))] pub async fn info_refs( req: HttpRequest, path: web::Path<(String, String)>, state: web::Data, ) -> Result { if !state.rate_limiter.is_read_allowed().await { return Err(actix_web::error::ErrorTooManyRequests( "Rate limit exceeded", )); } let service_param = req .query_string() .split('&') .find(|s| s.starts_with("service=")) .and_then(|s| s.strip_prefix("service=")) .ok_or_else(|| { actix_web::error::ErrorBadRequest("Missing service parameter") })?; if service_param != "git-upload-pack" && service_param != "git-receive-pack" { return Err(actix_web::error::ErrorBadRequest("Invalid service")); } let path_inner = path.into_inner(); let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; let is_write = service_param == "git-receive-pack"; authorize_repo_access(&req, &state.db, &model, is_write).await?; let storage_path = PathBuf::from(&model.storage_path); let handler = GitHttpHandler::new(storage_path, model, state.db.clone()); handler.info_refs(service_param).await } #[tracing::instrument(skip(req, payload, state), fields(namespace = %path.0, repo = %path.1))] pub async fn upload_pack( req: HttpRequest, path: web::Path<(String, String)>, payload: web::Payload, state: web::Data, ) -> Result { if !state.rate_limiter.is_read_allowed().await { return Err(actix_web::error::ErrorTooManyRequests( "Rate limit exceeded", )); } let path_inner = path.into_inner(); let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; authorize_repo_access(&req, &state.db, &model, false).await?; let storage_path = PathBuf::from(&model.storage_path); let handler = GitHttpHandler::new(storage_path, model, state.db.clone()); handler.upload_pack(payload).await } #[tracing::instrument(skip(req, payload, state), fields(namespace = %path.0, repo = %path.1))] pub async fn receive_pack( req: HttpRequest, path: web::Path<(String, String)>, payload: web::Payload, state: web::Data, ) -> Result { if !state.rate_limiter.is_write_allowed().await { return Err(actix_web::error::ErrorTooManyRequests( "Rate limit exceeded", )); } let path_inner = path.into_inner(); let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; authorize_repo_access(&req, &state.db, &model, true).await?; let mut push_queue_lease = match wait_for_push_queue_slot( state.sync.clone(), model.id, |event, request_id| { let request_id = request_id.to_string(); let repo_name = model.name.clone(); let repo_id = model.id; match event { PushQueueEvent::Waiting(position) => { tracing::info!( repo = %repo_name, repo_id = %repo_id, request_id = %request_id, position = position.position, total = position.total, "http_push_queue_waiting" ); } PushQueueEvent::Acquired => { tracing::info!( repo = %repo_name, repo_id = %repo_id, request_id = %request_id, "http_push_queue_acquired" ); } } }, ) .await { Ok(lease) => lease, Err(PushQueueWaitError::Join(e)) => { tracing::error!( error = %e, repo = %model.name, repo_id = %model.id, "http_push_queue_join_failed" ); return Err(actix_web::error::ErrorServiceUnavailable( "Push queue is temporarily unavailable. Please retry later.", )); } Err(PushQueueWaitError::Lock(e)) => { tracing::error!( error = %e, repo = %model.name, repo_id = %model.id, "http_push_queue_lock_failed" ); return Err(actix_web::error::ErrorServiceUnavailable( "Push queue lock failed. Please retry later.", )); } Err(PushQueueWaitError::Timeout) => { tracing::info!( repo = %model.name, repo_id = %model.id, "http_push_queue_timeout" ); return Ok(HttpResponse::ServiceUnavailable() .insert_header(("Retry-After", "5")) .content_type("text/plain; charset=utf-8") .body("Push queue timed out. Please retry in a moment.\n")); } }; let storage_path = PathBuf::from(&model.storage_path); let handler = GitHttpHandler::new(storage_path, model.clone(), state.db.clone()); let result = handler.receive_pack(payload).await; push_queue_lease.release().await; if result.is_ok() { let _ = tokio::spawn({ let sync = state.sync.clone(); let repo_uid = model.id; async move { let _ = timeout( Duration::from_secs(5), sync.send(RepoReceiveSyncTask { repo_uid }), ) .await; } }); } result }