use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use super::metadata::{FileEntry, Manifest, PackMeta};
pub fn build_pack<P: AsRef<Path>, Q: AsRef<Path>>(input_dir: P, output_file: Q) -> Result<()> {
let input_dir = input_dir.as_ref();
let output_file = output_file.as_ref();
let pack_toml_path = input_dir.join("pack.toml");
if !pack_toml_path.exists() {
anyhow::bail!("pack.toml not found in {}", input_dir.display());
}
let pack_toml_content = std::fs::read_to_string(&pack_toml_path)
.with_context(|| format!("Failed to read pack.toml at {}", pack_toml_path.display()))?;
let pack_meta: PackMeta = toml::from_str(&pack_toml_content)
.with_context(|| format!("Failed to parse pack.toml at {}", pack_toml_path.display()))?;
let mut file_paths = collect_files(input_dir)?;
file_paths.sort();
let mut file_entries = Vec::new();
for file_path in &file_paths {
let full_path = input_dir.join(file_path);
let metadata = std::fs::metadata(&full_path)
.with_context(|| format!("Failed to read metadata for {}", full_path.display()))?;
let size = metadata.len();
let hash = compute_sha256(&full_path)
.with_context(|| format!("Failed to compute hash for {}", full_path.display()))?;
let normalized_path = file_path.to_string_lossy().replace('\\', "/");
file_entries.push(FileEntry {
path: normalized_path,
size,
sha256: hash,
});
}
let manifest = Manifest {
pack: pack_meta.clone(),
files: file_entries,
};
let manifest_toml = toml::to_string_pretty(&manifest)
.context("Failed to serialize manifest to TOML")?;
let tar_data = create_tar_archive(input_dir, &file_paths, &manifest_toml)
.context("Failed to create tar archive")?;
let compressed = zstd::encode_all(&tar_data[..], 3)
.context("Failed to compress archive with zstd")?;
std::fs::write(output_file, compressed)
.with_context(|| format!("Failed to write output file {}", output_file.display()))?;
println!("✓ Pack built successfully: {}", output_file.display());
println!(" Pack ID: {}", pack_meta.pack_id);
println!(" Version: {}", pack_meta.version);
println!(" Files: {}", file_paths.len());
Ok(())
}
fn collect_files(input_dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for entry in WalkDir::new(input_dir)
.into_iter()
.filter_entry(|e| !is_excluded(e))
{
let entry = entry.context("Failed to read directory entry")?;
if entry.file_type().is_dir() {
continue;
}
let relative_path = entry
.path()
.strip_prefix(input_dir)
.context("Failed to compute relative path")?
.to_path_buf();
files.push(relative_path);
}
Ok(files)
}
fn is_excluded(entry: &walkdir::DirEntry) -> bool {
let name = entry.file_name().to_string_lossy();
if name.starts_with('.') {
return true;
}
let excluded_names = [
"node_modules",
"target",
"dist",
"build",
"__pycache__",
".DS_Store",
"Thumbs.db",
];
if excluded_names.contains(&name.as_ref()) {
return true;
}
if name.ends_with('~') || name.ends_with(".bak") || name.ends_with(".swp") {
return true;
}
false
}
fn compute_sha256(path: &Path) -> Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn create_tar_archive(
input_dir: &Path,
file_paths: &[PathBuf],
manifest_toml: &str,
) -> Result<Vec<u8>> {
let mut tar_data = Vec::new();
{
let mut tar = tar::Builder::new(&mut tar_data);
for file_path in file_paths {
let full_path = input_dir.join(file_path);
let mut file = File::open(&full_path)
.with_context(|| format!("Failed to open file {}", full_path.display()))?;
let normalized_path = file_path.to_string_lossy().replace('\\', "/");
tar.append_file(&normalized_path, &mut file)
.with_context(|| format!("Failed to add {} to archive", normalized_path))?;
}
let manifest_bytes = manifest_toml.as_bytes();
let mut header = tar::Header::new_gnu();
header.set_path("manifest.toml")?;
header.set_size(manifest_bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append(&header, manifest_bytes)
.context("Failed to add manifest.toml to archive")?;
tar.finish().context("Failed to finalize tar archive")?;
}
Ok(tar_data)
}