use std::path::PathBuf; use export::storage::AppStorage; use gitbaby::archive::types::{ArchiveFormat, CreateArchiveRequest}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::service::GitService; use crate::service::cache::sanitize_key; use crate::service::gitcmd::{run_git, run_git_capped}; use crate::share::errors::{GitError, map_baby}; use crate::share::validate::validate_revision; #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct ArchiveResultDto { pub format: String, pub size: i64, pub cache_key: String, pub download_url: Option, } #[derive(Debug, Clone, Deserialize)] pub struct ArchiveOptionsDto { pub format: ArchiveFormat, #[serde(default)] pub path_filter: Option, } fn fmt_ext(f: ArchiveFormat) -> &'static str { match f { ArchiveFormat::Tar => "tar", ArchiveFormat::TarGz => "tar.gz", ArchiveFormat::TarBz2 => "tar.bz2", ArchiveFormat::Zip => "zip", } } impl GitService { pub async fn archive_create( &self, space_name: String, repo_name: String, revision: String, opts: ArchiveOptionsDto, ) -> Result { validate_revision(&revision)?; let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let ext = fmt_ext(opts.format); let prefix_filter = opts.path_filter.clone(); let req = CreateArchiveRequest { commit: revision.clone(), format: opts.format, prefix: Some(format!("{}-", ctx.model.id.simple())), paths: prefix_filter.into_iter().collect(), }; baby.create_archive(req).await.map_err(map_baby)?; let key = format!( "git/{}/{}.{}", ctx.model.id.simple(), sanitize_key(&revision), ext ); let out_dir = ctx.repo_dir(); let storage: &AppStorage = &self.state.storage; let timeout = self.state.config.git.git_command_timeout_secs; let bytes = read_archive_bytes(&out_dir, &revision, ext, timeout).await?; let bytes_len = bytes.len() as i64; storage .client() .put(&key, &bytes) .await .map_err(|e| GitError::Internal(format!("archive put failed: {e}")))?; let download_url = storage.client().presign_get(&key, 3600).ok(); Ok(ArchiveResultDto { format: ext.to_string(), size: bytes_len, cache_key: key, download_url, }) } pub async fn archive_download( &self, space_name: String, repo_name: String, revision: String, opts: ArchiveOptionsDto, ) -> Result { validate_revision(&revision)?; let ctx = self.context(space_name, repo_name).await?; let ext = fmt_ext(opts.format); let key = format!( "git/{}/{}.{}", ctx.model.id.simple(), sanitize_key(&revision), ext ); let storage: &AppStorage = &self.state.storage; let head = storage .client() .head(&key) .await .map_err(|e| GitError::Internal(format!("storage head: {e}")))?; let Some((size, _)) = head else { return Err(GitError::NotFound); }; let download_url = storage.client().presign_get(&key, 3600).ok(); Ok(ArchiveResultDto { format: ext.to_string(), size: size as i64, cache_key: key, download_url, }) } pub async fn archive_create_bundle( &self, space_name: String, repo_name: String, ) -> Result { let ctx = self.context(space_name, repo_name).await?; let bundle_path = std::env::temp_dir().join(format!( "gitdata-bundle-{}-{}", std::process::id(), OffsetDateTime::now_utc().unix_timestamp_nanos() )); let timeout = self.state.config.git.git_command_timeout_secs; run_git( "archives::bundle", &ctx.repo_dir(), vec![ "bundle".to_string(), "create".to_string(), bundle_path.to_string_lossy().into_owned(), "--all".to_string(), ], &[], timeout, ) .await?; let key = format!("git/{}/bundle.git", ctx.model.id.simple()); let storage: &AppStorage = &self.state.storage; let bytes = tokio::task::spawn_blocking({ let p = bundle_path.clone(); move || std::fs::read(&p) }) .await .map_err(|e| GitError::Internal(format!("bundle read task: {e}")))? .map_err(|e| GitError::Internal(format!("bundle read: {e}")))?; let bytes_len = bytes.len() as i64; storage .client() .put(&key, &bytes) .await .map_err(|e| GitError::Internal(format!("bundle put failed: {e}")))?; let download_url = storage.client().presign_get(&key, 3600).ok(); let _ = std::fs::remove_file(&bundle_path); Ok(ArchiveResultDto { format: "bundle".to_string(), size: bytes_len, cache_key: key, download_url, }) } } async fn read_archive_bytes( dir: &PathBuf, rev: &str, ext: &str, timeout_secs: u64, ) -> Result, GitError> { let cap = 512 * 1024 * 1024; let out = run_git_capped( "archives::read", dir, vec![ "archive".to_string(), "--format".to_string(), ext.to_string(), rev.to_string(), ], &[], timeout_secs, cap, ) .await?; Ok(out.stdout) }