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, #[serde(default)] pub max_depth: Option, #[serde(default)] pub path_filter: Option, } impl ArchiveQuery { fn to_archive_format(&self) -> Result { 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, 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 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 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 { 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::(&cached) { return Ok(cached); } } } let domain = git::GitDomain::from_model(repo)?; let opts = git::ArchiveOptions::new() .prefix(query.prefix.as_deref().unwrap_or("")) .max_depth(query.max_depth.unwrap_or(usize::MAX)); let data = domain.archive(&commit_oid, format, Some(opts))?; 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::( cache_key, serde_json::to_string(&response).unwrap_or_default(), 60 * 60, ) .await { slog::debug!(self.logs, "cache set failed (non-fatal): {}", e); } } Ok(response) } pub async fn git_archive_list( &self, namespace: String, repo_name: String, query: ArchiveQuery, ctx: &Session, ) -> Result { let repo = self .utils_find_repo(namespace.clone(), repo_name.clone(), ctx) .await?; 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 entries = domain.archive_list(&commit_oid, Some(opts))?; let entry_responses: Vec = entries .into_iter() .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 { 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 mut summary = domain.archive_summary(&commit_oid, format, Some(opts))?; 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 { 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 { 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 { 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 }) } }