use crate::acquire::download::Downloader;
use crate::acquire::sources::RepoSource;
use crate::acquire::strategy::{AcquireOptions, AcquisitionStrategy};
use crate::archive::{ArchiveValidator, SafeExtractor};
use crate::auth;
use crate::core::{AcquisitionReport, Config, ScanReport};
use crate::git::GitSanitizer;
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
use tracing::info;
pub struct ZipWithHistoryStrategy {
config: Config,
downloader: Downloader,
}
impl ZipWithHistoryStrategy {
pub fn new(config: Config) -> Self {
Self {
config,
downloader: Downloader::new(),
}
}
}
#[async_trait]
impl AcquisitionStrategy for ZipWithHistoryStrategy {
async fn acquire(
&self,
source: &RepoSource,
target: &Path,
opts: &AcquireOptions,
) -> Result<AcquisitionReport> {
let zip_url = source.zip_url()?;
let temp_zip = tempfile::Builder::new()
.prefix("securegit-")
.suffix(".zip")
.tempfile()?;
let headers = if let Some(ref tok) = opts.token {
let host = source.host().unwrap_or_default();
auth::build_http_headers(Some(tok), &host)
} else {
let host = source.host().unwrap_or_default();
auth::build_http_headers(None, &host)
};
self.downloader
.download_with_headers(&zip_url, temp_zip.path(), &headers)
.await?;
let validator = ArchiveValidator::new(self.config.archive.clone());
let extractor = SafeExtractor::new(validator);
let temp_dir = tempfile::tempdir()?;
extractor
.extract_safe(temp_zip.path(), temp_dir.path())
.await?;
let clone_url = source.clone_url()?;
let bare_dir = tempfile::tempdir()?;
let bare_path = bare_dir.path().to_path_buf();
let clone_url_owned = clone_url.clone();
let token_clone = opts.token.clone();
let ssh_key_clone = opts.ssh_key.clone();
let depth = opts.depth;
tokio::task::spawn_blocking(move || -> Result<()> {
let mut builder = git2::build::RepoBuilder::new();
builder.bare(true);
let callbacks =
auth::build_git2_callbacks(token_clone.as_ref(), ssh_key_clone.as_deref(), None);
let mut fetch_opts = git2::FetchOptions::new();
fetch_opts.remote_callbacks(callbacks);
if let Some(d) = depth {
fetch_opts.depth(d as i32);
}
builder.fetch_options(fetch_opts);
builder.clone(&clone_url_owned, &bare_path)?;
Ok(())
})
.await??;
let sanitizer = GitSanitizer::new(self.config.sanitization.clone());
let sanitize_report = sanitizer.sanitize(bare_dir.path())?;
std::fs::create_dir_all(target)?;
copy_dir_all(temp_dir.path(), target)?;
crate::git::convert::convert_bare_to_git_dir(bare_dir.path(), &target.join(".git"))?;
if opts.recurse_submodules {
info!("Recursively acquiring submodules...");
let max_depth = self.config.submodules.max_depth;
match crate::acquire::submodules::acquire_submodules(
target,
opts.token.as_ref(),
opts.ssh_key.as_deref(),
max_depth,
)
.await
{
Ok(count) => {
if count > 0 {
info!("Acquired {} submodule(s)", count);
}
}
Err(e) => {
tracing::warn!("Submodule acquisition failed: {}", e);
}
}
}
if opts.lfs {
info!("Resolving LFS pointers...");
let clone_url = source.clone_url()?;
match crate::lfs::resolve_lfs_pointers(target, &clone_url, opts.token.as_ref(), true)
.await
{
Ok(count) => {
if count > 0 {
info!("Resolved {} LFS object(s)", count);
}
}
Err(e) => {
tracing::warn!("LFS resolution failed: {}", e);
}
}
}
Ok(AcquisitionReport {
target: target.to_path_buf(),
scan_report: ScanReport::new(),
sanitize_report,
has_history: true,
head_commit: None,
})
}
}
fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}