use db::sqlx; use git::rpc::{proto as p, proto::blob_service_client::BlobServiceClient}; use session::Session; use crate::{AppService, error::AppError, git::rpc_err}; #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] pub struct ContentResponse { pub path: String, pub name: String, #[serde(rename = "type")] pub content_type: String, pub size: i64, pub encoding: Option, pub content: Option, } #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] pub struct CreateContent { pub message: String, pub content: String, pub branch: Option, } #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] pub struct UpdateContent { pub message: String, pub content: String, pub sha: String, pub branch: Option, } impl AppService { pub async fn git_contents_get_by_name( &self, ctx: &Session, wk: &str, repo: &str, path: &str, ref_name: Option<&str>, ) -> Result { let _ = self.git_require_member(ctx, wk, repo).await?; self.git_contents_get(ctx, wk, repo, path, ref_name).await } pub async fn git_contents_create_by_name( &self, ctx: &Session, wk: &str, repo: &str, path: &str, params: CreateContent, ) -> Result { let _ = self.git_require_member(ctx, wk, repo).await?; self.git_contents_create(ctx, wk, repo, path, params).await } pub async fn git_contents_update_by_name( &self, ctx: &Session, wk: &str, repo: &str, path: &str, params: UpdateContent, ) -> Result { let _ = self.git_require_member(ctx, wk, repo).await?; self.git_contents_update(ctx, wk, repo, path, params).await } pub async fn git_contents_delete_by_name( &self, ctx: &Session, wk: &str, repo: &str, path: &str, msg: &str, sha: &str, branch: Option<&str>, ) -> Result<(), AppError> { let _ = self.git_require_member(ctx, wk, repo).await?; self.git_contents_delete(ctx, wk, repo, path, msg, sha, branch) .await } } impl AppService { pub async fn git_contents_get( &self, ctx: &Session, wk_name: &str, repo_name: &str, path: &str, _ref_name: Option<&str>, ) -> Result { let repo = self.git_require_member(ctx, wk_name, repo_name).await?; let mut blob_client = BlobServiceClient::new(self.git.clone()); let empty_oid = p::ObjectId { value: String::new(), }; let resp = blob_client .blob_load(tonic::Request::new(p::BlobLoadRequest { repo_id: repo.id.to_string(), id: Some(empty_oid.clone()), path: path.to_string(), })) .await .map_err(rpc_err)? .into_inner(); let is_binary = blob_client .blob_is_binary(tonic::Request::new(p::BlobIsBinaryRequest { repo_id: repo.id.to_string(), id: Some(empty_oid.clone()), })) .await .map(|r| r.into_inner().is_binary) .unwrap_or(false); let size_resp = blob_client .blob_size(tonic::Request::new(p::BlobSizeRequest { repo_id: repo.id.to_string(), id: Some(empty_oid), path: String::new(), })) .await .map_err(rpc_err)? .into_inner(); let blob_data = resp.blob; let size = size_resp.size as i64; let (encoding, content) = if is_binary { (Some("base64".to_string()), Some(base64_encode(&blob_data))) } else { (None, Some(String::from_utf8_lossy(&blob_data).to_string())) }; Ok(ContentResponse { path: path.to_string(), name: path.rsplit('/').next().unwrap_or(path).to_string(), content_type: "file".to_string(), size, encoding, content, }) } pub async fn git_contents_create( &self, ctx: &Session, wk_name: &str, repo_name: &str, path: &str, params: CreateContent, ) -> Result { let repo = self.git_require_member(ctx, wk_name, repo_name).await?; let user = crate::session_user(ctx)?; let user_model = sqlx::query_as::<_, model::users::UserModel>( "SELECT id, username, display_name, avatar_url, website_url, allow_use, can_search, \ last_sign_in_at, created_at, updated_at FROM \"user\" WHERE id = $1", ) .bind(user) .fetch_optional(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::UserNotFound)?; let display = user_model.display_name.clone(); let username = user_model.username.clone(); let author_name = if display.is_empty() { username.clone() } else { display }; let file_size = params.content.len() as i64; let content_bytes = params.content.clone().into_bytes(); let mut client = p::commit_service_client::CommitServiceClient::new( self.git.clone(), ); let resp = client .create_commit(tonic::Request::new(p::CreateCommitRequest { repo_id: repo.id.to_string(), branch: params .branch .unwrap_or_else(|| repo.default_branch.clone()), message: params.message, author_name: author_name.clone(), author_email: format!("{username}@gitdata.ai"), committer_name: "redpanda".to_string(), committer_email: "redpanda@gitdata.ai".to_string(), files: vec![p::FileChange { path: path.to_string(), content: content_bytes, }], })) .await .map_err(rpc_err)? .into_inner(); let _oid = resp.oid.map(|o| o.value).unwrap_or_default(); Ok(ContentResponse { path: path.to_string(), name: path.rsplit('/').next().unwrap_or(path).to_string(), content_type: "file".to_string(), size: file_size, encoding: None, content: Some(params.content), }) } pub async fn git_contents_update( &self, _ctx: &Session, _wk: &str, _repo: &str, _path: &str, _params: UpdateContent, ) -> Result { Err(AppError::InternalServerError( "contents update not yet implemented".to_string(), )) } pub async fn git_contents_delete( &self, _ctx: &Session, _wk: &str, _repo: &str, _path: &str, _message: &str, _sha: &str, _branch: Option<&str>, ) -> Result<(), AppError> { Err(AppError::InternalServerError( "contents delete not yet implemented".to_string(), )) } } fn base64_encode(data: &[u8]) -> String { use base64::Engine as _; base64::engine::general_purpose::STANDARD.encode(data) }