gitdataai/libs/service/git/archive.rs

349 lines
13 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use crate::git::{ArchiveEntry, ArchiveFormat, ArchiveSummary};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub struct ArchiveQuery {
pub commit_oid: String,
pub format: String,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub max_depth: Option<usize>,
#[serde(default)]
pub path_filter: Option<String>,
}
impl ArchiveQuery {
fn to_archive_format(&self) -> Result<ArchiveFormat, AppError> {
match self.format.to_lowercase().as_str() {
"tar" => Ok(ArchiveFormat::Tar),
"tar.gz" | "tgz" => Ok(ArchiveFormat::TarGz),
"zip" => Ok(ArchiveFormat::Zip),
_ => Err(AppError::InternalServerError(format!(
"unsupported archive format: {}",
self.format
))),
}
}
fn cache_key(&self) -> String {
let prefix = self.prefix.as_deref().unwrap_or("");
let filter = self.path_filter.as_deref().unwrap_or("");
let depth = self.max_depth.map_or("0".to_string(), |d| d.to_string());
if prefix.is_empty() && filter.is_empty() && self.max_depth.is_none() {
String::new()
} else {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
(prefix, filter, depth).hash(&mut h);
format!("-{:x}", h.finish())
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveListResponse {
pub commit_oid: String,
pub entries: Vec<ArchiveEntryResponse>,
pub total_entries: usize,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveEntryResponse {
pub path: String,
pub oid: String,
pub size: u64,
pub mode: u32,
}
impl From<ArchiveEntry> for ArchiveEntryResponse {
fn from(e: ArchiveEntry) -> Self {
Self {
path: e.path,
oid: e.oid,
size: e.size,
mode: e.mode,
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveSummaryResponse {
pub commit_oid: String,
pub format: String,
pub total_entries: usize,
pub total_size: u64,
}
impl From<ArchiveSummary> for ArchiveSummaryResponse {
fn from(s: ArchiveSummary) -> Self {
let format_str = match s.format {
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::Zip => "zip",
};
Self {
commit_oid: s.commit_oid,
format: format_str.to_string(),
total_entries: s.total_entries,
total_size: s.total_size,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ArchiveResponse {
pub commit_oid: String,
pub format: String,
pub size: usize,
pub data: String,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveCachedResponse {
pub commit_oid: String,
pub format: String,
pub cached: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveInvalidateResponse {
pub commit_oid: String,
pub format: String,
pub invalidated: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct ArchiveInvalidateAllResponse {
pub commit_oid: String,
pub count: usize,
}
impl AppService {
pub async fn git_archive(
&self,
namespace: String,
repo_name: String,
query: ArchiveQuery,
ctx: &Session,
) -> Result<ArchiveResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let format = query.to_archive_format()?;
let format_str = match format {
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::Zip => "zip",
};
let _commit_oid = git::CommitOid::new(&query.commit_oid);
let cache_key = format!(
"git:archive:{}:{}:{}:{}:{}",
namespace,
repo_name,
query.commit_oid,
format_str,
query.cache_key(),
);
if let Ok(mut conn) = self.cache.conn().await {
if let Ok(cached) = conn.get::<_, String>(cache_key.clone()).await {
if let Ok(cached) = serde_json::from_str::<ArchiveResponse>(&cached) {
return Ok(cached);
}
}
}
let _opts = git::ArchiveOptions::new()
.prefix(query.prefix.as_deref().unwrap_or(""))
.max_depth(query.max_depth.unwrap_or(usize::MAX));
let arch_prefix = query.prefix.clone();
let arch_max_depth = query.max_depth;
let arch_commit_oid = query.commit_oid.clone();
let arch_format_str = format_str.to_string();
let data = tokio::task::spawn_blocking(move || {
let domain = git::GitDomain::from_model(repo)?;
let commit_oid = git::CommitOid::new(&arch_commit_oid);
let format = match arch_format_str.as_str() {
"tar" => ArchiveFormat::Tar,
"tar.gz" => ArchiveFormat::TarGz,
"zip" => ArchiveFormat::Zip,
_ => unreachable!(),
};
let opts = git::ArchiveOptions::new()
.prefix(arch_prefix.as_deref().unwrap_or(""))
.max_depth(arch_max_depth.unwrap_or(usize::MAX));
domain.archive(&commit_oid, format, Some(opts))
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))??;
let data_b64 = BASE64.encode(&data);
let response = ArchiveResponse {
commit_oid: query.commit_oid,
format: format_str.to_string(),
size: data.len(),
data: data_b64,
};
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
60 * 60,
)
.await
{
tracing::debug!(error = ?e, "cache set failed (non-fatal)");
}
}
Ok(response)
}
pub async fn git_archive_list(
&self,
namespace: String,
repo_name: String,
query: ArchiveQuery,
ctx: &Session,
) -> Result<ArchiveListResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let list_commit_oid = query.commit_oid.clone();
let list_prefix = query.prefix.clone();
let list_max_depth = query.max_depth;
let entries = tokio::task::spawn_blocking(move || {
let domain = git::GitDomain::from_model(repo)?;
let commit_oid = git::CommitOid::new(&list_commit_oid);
let opts = git::ArchiveOptions::new()
.prefix(list_prefix.as_deref().unwrap_or(""))
.max_depth(list_max_depth.unwrap_or(usize::MAX));
domain.archive_list(&commit_oid, Some(opts))
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))??;
let max_entries = 10000;
let entry_responses: Vec<ArchiveEntryResponse> = entries
.into_iter()
.take(max_entries)
.map(ArchiveEntryResponse::from)
.collect();
let total_entries = entry_responses.len();
Ok(ArchiveListResponse {
commit_oid: query.commit_oid,
entries: entry_responses,
total_entries,
})
}
pub async fn git_archive_summary(
&self,
namespace: String,
repo_name: String,
query: ArchiveQuery,
ctx: &Session,
) -> Result<ArchiveSummaryResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let format = query.to_archive_format()?;
let format_str = match format {
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::Zip => "zip",
};
let _commit_oid = git::CommitOid::new(&query.commit_oid);
let _opts = git::ArchiveOptions::new()
.prefix(query.prefix.as_deref().unwrap_or(""))
.max_depth(query.max_depth.unwrap_or(usize::MAX));
let sum_commit_oid = query.commit_oid.clone();
let sum_prefix = query.prefix.clone();
let sum_max_depth = query.max_depth;
let sum_format_str = format_str.to_string();
let mut summary = tokio::task::spawn_blocking(move || {
let domain = git::GitDomain::from_model(repo)?;
let commit_oid = git::CommitOid::new(&sum_commit_oid);
let format = match sum_format_str.as_str() {
"tar" => ArchiveFormat::Tar,
"tar.gz" => ArchiveFormat::TarGz,
"zip" => ArchiveFormat::Zip,
_ => ArchiveFormat::Tar,
};
let opts = git::ArchiveOptions::new()
.prefix(sum_prefix.as_deref().unwrap_or(""))
.max_depth(sum_max_depth.unwrap_or(usize::MAX));
domain.archive_summary(&commit_oid, format, Some(opts))
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))??;
summary.format = format;
Ok(ArchiveSummaryResponse {
commit_oid: query.commit_oid,
format: format_str.to_string(),
total_entries: summary.total_entries,
total_size: summary.total_size,
})
}
pub async fn git_archive_cached(
&self,
namespace: String,
repo_name: String,
query: ArchiveQuery,
ctx: &Session,
) -> Result<ArchiveCachedResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let format = query.to_archive_format()?;
let format_str = match format {
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::Zip => "zip",
};
let commit_oid = git::CommitOid::new(&query.commit_oid);
let opts = git::ArchiveOptions::new()
.prefix(query.prefix.as_deref().unwrap_or(""))
.max_depth(query.max_depth.unwrap_or(usize::MAX));
let domain = git::GitDomain::from_model(repo)?;
let cached = domain.archive_cached(&commit_oid, format, Some(opts));
Ok(ArchiveCachedResponse {
commit_oid: query.commit_oid,
format: format_str.to_string(),
cached,
})
}
pub async fn git_archive_invalidate(
&self,
namespace: String,
repo_name: String,
query: ArchiveQuery,
ctx: &Session,
) -> Result<ArchiveInvalidateResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let format = query.to_archive_format()?;
let format_str = match format {
ArchiveFormat::Tar => "tar",
ArchiveFormat::TarGz => "tar.gz",
ArchiveFormat::Zip => "zip",
};
let commit_oid = git::CommitOid::new(&query.commit_oid);
let opts = git::ArchiveOptions::new()
.prefix(query.prefix.as_deref().unwrap_or(""))
.max_depth(query.max_depth.unwrap_or(usize::MAX));
let domain = git::GitDomain::from_model(repo)?;
let invalidated = domain.archive_invalidate(&commit_oid, format, Some(opts))?;
Ok(ArchiveInvalidateResponse {
commit_oid: query.commit_oid,
format: format_str.to_string(),
invalidated,
})
}
pub async fn git_archive_invalidate_all(
&self,
namespace: String,
repo_name: String,
commit_oid: String,
ctx: &Session,
) -> Result<ArchiveInvalidateAllResponse, AppError> {
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let commit = git::CommitOid::new(&commit_oid);
let domain = git::GitDomain::from_model(repo)?;
let count = domain.archive_invalidate_all(&commit)?;
Ok(ArchiveInvalidateAllResponse { commit_oid, count })
}
}