use flate2::Compression;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tar::{Archive, Builder, Header};
use crate::error::{CoreError, Result};
use crate::manifest::Manifest;
use crate::pack::LoadedPack;
pub fn create_archive(pack: &LoadedPack, output: &Path) -> Result<PathBuf> {
let manifest = Manifest::generate(pack)?;
let manifest_content = manifest.to_string();
let file = File::create(output)?;
let encoder = GzEncoder::new(file, Compression::default());
let mut builder = Builder::new(encoder);
add_bytes_to_archive(&mut builder, "MANIFEST", manifest_content.as_bytes())?;
let pack_yaml = pack.root.join("Pack.yaml");
if pack_yaml.exists() {
add_file_to_archive(&mut builder, &pack_yaml, "Pack.yaml")?;
}
if pack.values_path.exists() {
add_file_to_archive(&mut builder, &pack.values_path, "values.yaml")?;
}
if let Some(schema_path) = &pack.schema_path
&& schema_path.exists()
{
let schema_name = schema_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "values.schema.yaml".to_string());
add_file_to_archive(&mut builder, schema_path, &schema_name)?;
}
let template_files = pack.template_files()?;
for file_path in template_files {
let rel_path = file_path
.strip_prefix(&pack.root)
.unwrap_or(&file_path)
.to_string_lossy()
.replace('\\', "/");
add_file_to_archive(&mut builder, &file_path, &rel_path)?;
}
let encoder = builder.into_inner()?;
encoder.finish()?;
Ok(output.to_path_buf())
}
pub fn extract_archive(archive_path: &Path, dest: &Path) -> Result<()> {
let file = File::open(archive_path)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
std::fs::create_dir_all(dest)?;
archive.unpack(dest)?;
Ok(())
}
pub fn list_archive(archive_path: &Path) -> Result<Vec<ArchiveEntry>> {
let file = File::open(archive_path)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
let mut entries = Vec::new();
for entry in archive.entries()? {
let entry = entry?;
let path = entry.path()?.to_string_lossy().to_string();
let size = entry.header().size()?;
let is_dir = entry.header().entry_type().is_dir();
entries.push(ArchiveEntry { path, size, is_dir });
}
Ok(entries)
}
pub fn read_file_from_archive(archive_path: &Path, file_path: &str) -> Result<Vec<u8>> {
let file = File::open(archive_path)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().to_string();
if path == file_path {
let mut content = Vec::new();
entry.read_to_end(&mut content)?;
return Ok(content);
}
}
Err(CoreError::Archive {
message: format!("File not found in archive: {}", file_path),
})
}
pub fn read_manifest_from_archive(archive_path: &Path) -> Result<Manifest> {
let content = read_file_from_archive(archive_path, "MANIFEST")?;
let text = String::from_utf8(content).map_err(|e| CoreError::Archive {
message: format!("Invalid UTF-8 in MANIFEST: {}", e),
})?;
Manifest::parse(&text)
}
fn read_all_files_from_archive(archive_path: &Path) -> Result<HashMap<String, Vec<u8>>> {
let file = File::open(archive_path)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
let mut contents = HashMap::new();
for entry in archive.entries()? {
let mut entry = entry?;
if entry.header().entry_type().is_dir() {
continue;
}
let path = entry.path()?.to_string_lossy().to_string();
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
contents.insert(path, data);
}
Ok(contents)
}
pub fn verify_archive(archive_path: &Path) -> Result<crate::manifest::VerificationResult> {
let manifest = read_manifest_from_archive(archive_path)?;
let file_contents = read_all_files_from_archive(archive_path)?;
manifest.verify_files(|path| {
file_contents
.get(path)
.cloned()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"))
})
}
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub path: String,
pub size: u64,
pub is_dir: bool,
}
fn add_file_to_archive<W: Write>(
builder: &mut Builder<W>,
file_path: &Path,
archive_path: &str,
) -> Result<()> {
let content = std::fs::read(file_path)?;
add_bytes_to_archive(builder, archive_path, &content)
}
fn add_bytes_to_archive<W: Write>(
builder: &mut Builder<W>,
archive_path: &str,
content: &[u8],
) -> Result<()> {
let mut header = Header::new_gnu();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_mtime(0); header.set_cksum();
builder.append_data(&mut header, archive_path, content)?;
Ok(())
}
#[must_use]
pub fn default_archive_name(pack: &LoadedPack) -> String {
format!(
"{}-{}.tar.gz",
pack.pack.metadata.name, pack.pack.metadata.version
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_pack(dir: &Path) {
std::fs::write(
dir.join("Pack.yaml"),
r#"apiVersion: sherpack/v1
kind: application
metadata:
name: testpack
version: 1.0.0
"#,
)
.unwrap();
std::fs::write(dir.join("values.yaml"), "replicas: 3\n").unwrap();
let templates_dir = dir.join("templates");
std::fs::create_dir_all(&templates_dir).unwrap();
std::fs::write(
templates_dir.join("deployment.yaml"),
"apiVersion: apps/v1\nkind: Deployment\n",
)
.unwrap();
}
#[test]
fn test_create_and_extract_archive() {
let temp = TempDir::new().unwrap();
let pack_dir = temp.path().join("pack");
std::fs::create_dir_all(&pack_dir).unwrap();
create_test_pack(&pack_dir);
let pack = LoadedPack::load(&pack_dir).unwrap();
let archive_path = temp.path().join("test.tar.gz");
create_archive(&pack, &archive_path).unwrap();
assert!(archive_path.exists());
let entries = list_archive(&archive_path).unwrap();
let paths: Vec<_> = entries.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"MANIFEST"));
assert!(paths.contains(&"Pack.yaml"));
assert!(paths.contains(&"values.yaml"));
assert!(paths.iter().any(|p| p.contains("deployment.yaml")));
let extract_dir = temp.path().join("extracted");
extract_archive(&archive_path, &extract_dir).unwrap();
assert!(extract_dir.join("MANIFEST").exists());
assert!(extract_dir.join("Pack.yaml").exists());
assert!(extract_dir.join("values.yaml").exists());
}
#[test]
fn test_read_manifest_from_archive() {
let temp = TempDir::new().unwrap();
let pack_dir = temp.path().join("pack");
std::fs::create_dir_all(&pack_dir).unwrap();
create_test_pack(&pack_dir);
let pack = LoadedPack::load(&pack_dir).unwrap();
let archive_path = temp.path().join("test.tar.gz");
create_archive(&pack, &archive_path).unwrap();
let manifest = read_manifest_from_archive(&archive_path).unwrap();
assert_eq!(manifest.name, "testpack");
assert_eq!(manifest.pack_version.to_string(), "1.0.0");
}
#[test]
fn test_verify_archive() {
let temp = TempDir::new().unwrap();
let pack_dir = temp.path().join("pack");
std::fs::create_dir_all(&pack_dir).unwrap();
create_test_pack(&pack_dir);
let pack = LoadedPack::load(&pack_dir).unwrap();
let archive_path = temp.path().join("test.tar.gz");
create_archive(&pack, &archive_path).unwrap();
let result = verify_archive(&archive_path).unwrap();
assert!(result.valid);
assert!(result.mismatched.is_empty());
assert!(result.missing.is_empty());
}
#[test]
fn test_default_archive_name() {
let temp = TempDir::new().unwrap();
create_test_pack(temp.path());
let pack = LoadedPack::load(temp.path()).unwrap();
let name = default_archive_name(&pack);
assert_eq!(name, "testpack-1.0.0.tar.gz");
}
}