ef0b83a
use crate::rpcgit::oid::ObjectId;
use serde::{Deserialize, Serialize};
use crate::context::facade::GitContext;
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<String>,
pub size: Option<i64>,
}
#[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<LfsPointerDto>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct BlobContentDto {
pub oid: String,
pub data: Vec<u8>,
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<LfsPointerDto> {
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::<i64>().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<u8>), 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,
ctx: &GitContext,
oid: String,
) -> Result<BlobMetaDto, GitError> {
let object_id = parse_oid(&oid)?;
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,
ctx: &GitContext,
oid: String,
) -> Result<BlobContentDto, GitError> {
let object_id = parse_oid(&oid)?;
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,
ctx: &GitContext,
oid: String,
) -> Result<RawDownloadDto, GitError> {
let object_id = parse_oid(&oid)?;
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,
ctx: &GitContext,
revision: String,
limit: Option<u32>,
) -> Result<Vec<LfsPointerDto>, GitError> {
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, GitError> {
ObjectId::from_hex(hex.trim().as_bytes())
.map_err(|_| GitError::InvalidArgument(format!("invalid object id `{hex}`")))
}