use crate::rpcgit::oid::ObjectId; use serde::{Deserialize, Serialize}; use crate::service::GitService; use crate::service::cache::{self, IMMUTABLE_TTL}; use crate::share::errors::{GitError, map_baby}; pub const LFS_POINTER_KEYWORD: &str = "https://git-lfs.github.com/spec/v1"; pub const SNIFF_LIMIT: i64 = 8192; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct LfsPointerDto { pub version: String, pub oid: Option, pub size: Option, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct BlobMetaDto { pub oid: String, pub size: i64, pub is_binary: bool, pub is_lfs_pointer: bool, pub lfs: Option, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct BlobContentDto { pub oid: String, pub data: Vec, pub size: i64, pub is_binary: bool, pub truncated: bool, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct RawDownloadDto { pub id: String, pub size: i64, pub is_lfs_pointer: bool, } pub fn is_binary(data: &[u8]) -> bool { let sniff = data.iter().take(SNIFF_LIMIT as usize); sniff.step_by(2).any(|&b| b == 0) } pub fn parse_lfs_pointer(data: &[u8]) -> Option { let text = String::from_utf8_lossy(data).to_string(); let mut version = None; let mut oid = None; let mut size = None; for line in text.lines().take(8) { if let Some(v) = line.strip_prefix("version ") { version = Some(v.to_string()); } else if let Some(v) = line.strip_prefix("oid sha256:") { oid = Some(v.trim().to_string()); } else if let Some(v) = line.strip_prefix("size ") { size = v.trim().parse::().ok(); } } match version { Some(v) if v == LFS_POINTER_KEYWORD => Some(LfsPointerDto { version: v, oid, size, }), _ => None, } } impl GitService { async fn blob_sniff( &self, ctx: &crate::context::facade::GitContext, oid: &ObjectId, ) -> Result<(i64, Vec), GitError> { let baby = self.baby(ctx); let size = baby.get_blob_size(oid.clone()).await.map_err(map_baby)?; let blob = baby .get_blob( oid.clone(), crate::rpcgit::blob::types::GetBlobOptions { limit: Some(SNIFF_LIMIT), }, ) .await .map_err(map_baby)?; Ok((size, blob.data)) } pub async fn blob_metadata( &self, space_name: String, repo_name: String, oid: String, ) -> Result { let object_id = parse_oid(&oid)?; let ctx = self.context(space_name, repo_name).await?; let key = format!("repo.{}.blob_meta.{}", ctx.model.id, object_id); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?; let lfs = parse_lfs_pointer(&sniff); Ok(BlobMetaDto { oid, size, is_binary: is_binary(&sniff), is_lfs_pointer: lfs.is_some(), lfs, }) }) .await } pub async fn blob_content( &self, space_name: String, repo_name: String, oid: String, ) -> Result { let object_id = parse_oid(&oid)?; let ctx = self.context(space_name, repo_name).await?; let max_blob = self.state.config.git.max_blob_size; let key = format!( "repo.{}.blob_content.{}:{}", ctx.model.id, object_id, max_blob ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let baby = self.baby(&ctx); let size = baby .get_blob_size(object_id.clone()) .await .map_err(map_baby)?; let truncated = size > max_blob as i64; let limit = if truncated { Some(max_blob as i64) } else { None }; let blob = baby .get_blob( object_id.clone(), crate::rpcgit::blob::types::GetBlobOptions { limit }, ) .await .map_err(map_baby)?; Ok(BlobContentDto { oid, size, is_binary: is_binary(&blob.data), data: blob.data, truncated, }) }) .await } pub async fn blob_raw_download( &self, space_name: String, repo_name: String, oid: String, ) -> Result { let object_id = parse_oid(&oid)?; let ctx = self.context(space_name, repo_name).await?; let key = format!("repo.{}.blob_raw_download.{}", ctx.model.id, object_id); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let (size, sniff) = self.blob_sniff(&ctx, &object_id).await?; Ok(RawDownloadDto { id: oid, size, is_lfs_pointer: parse_lfs_pointer(&sniff).is_some(), }) }) .await } pub async fn list_lfs_pointers( &self, space_name: String, repo_name: String, revision: String, limit: Option, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let pointers = baby .list_lfs_pointers(crate::rpcgit::blob::types::ListLFSPointersOptions { revisions: vec![revision], limit, }) .await .map_err(map_baby)?; Ok(pointers .into_iter() .map(|p| LfsPointerDto { version: LFS_POINTER_KEYWORD.to_string(), oid: Some(p.oid.to_string()), size: Some(p.size), }) .collect()) } } pub fn parse_oid(hex: &str) -> Result { ObjectId::from_hex(hex.trim().as_bytes()) .map_err(|_| GitError::InvalidArgument(format!("invalid object id `{hex}`"))) }