use std::path::{Path, PathBuf};
use greentic_deploy_spec::{LockedPack, PackId, PackListLock, RevisionId, SchemaVersion};
use sha2::{Digest, Sha256};
use crate::environment::atomic_write::atomic_write_json;
use super::OpError;
pub struct StagedBundle {
pub bundle_digest: String,
pub pack_list_lock_ref: PathBuf,
pub lock: PackListLock,
}
pub fn stage_local_bundle(
env_dir: &Path,
revision_id: RevisionId,
bundle_path: &Path,
) -> Result<StagedBundle, OpError> {
if !bundle_path.is_file() {
return Err(OpError::InvalidArgument(format!(
"bundle `{}` is not a file",
bundle_path.display()
)));
}
let rev_dir = env_dir.join("revisions").join(revision_id.to_string());
stage_into(env_dir, &rev_dir, revision_id, bundle_path).inspect_err(|_| {
let _ = std::fs::remove_dir_all(&rev_dir);
})
}
fn stage_into(
env_dir: &Path,
rev_dir: &Path,
revision_id: RevisionId,
bundle_path: &Path,
) -> Result<StagedBundle, OpError> {
let extract_dir = rev_dir.join("bundle");
if extract_dir.exists() {
std::fs::remove_dir_all(&extract_dir).map_err(|source| OpError::Io {
path: extract_dir.clone(),
source,
})?;
}
std::fs::create_dir_all(&extract_dir).map_err(|source| OpError::Io {
path: extract_dir.clone(),
source,
})?;
let staged_bundle = rev_dir.join("bundle.gtbundle");
std::fs::copy(bundle_path, &staged_bundle).map_err(|source| OpError::Io {
path: staged_bundle.clone(),
source,
})?;
let bundle_digest = sha256_file(&staged_bundle).map_err(|source| OpError::Io {
path: staged_bundle.clone(),
source,
})?;
greentic_bundle::build::unbundle_artifact(&staged_bundle, &extract_dir).map_err(|err| {
OpError::InvalidArgument(format!(
"extract bundle `{}`: {err:#}",
bundle_path.display()
))
})?;
if !extract_dir.join("bundle-manifest.json").is_file() {
return Err(OpError::InvalidArgument(format!(
"`{}` is not a .gtbundle: extracted tree has no bundle-manifest.json",
bundle_path.display()
)));
}
let packs_dir = extract_dir.join("packs");
if !packs_dir.is_dir() {
return Err(OpError::InvalidArgument(format!(
"bundle `{}` has no packs/ directory",
bundle_path.display()
)));
}
let mut gtpacks = Vec::new();
collect_gtpacks(&packs_dir, &mut gtpacks).map_err(|source| OpError::Io {
path: packs_dir.clone(),
source,
})?;
if gtpacks.is_empty() {
return Err(OpError::InvalidArgument(format!(
"bundle `{}` contains no .gtpack artifacts under packs/",
bundle_path.display()
)));
}
let mut packs = Vec::with_capacity(gtpacks.len());
for path in gtpacks {
let digest = sha256_file(&path).map_err(|source| OpError::Io {
path: path.clone(),
source,
})?;
let rel = path
.strip_prefix(env_dir)
.map_err(|_| {
OpError::InvalidArgument(format!(
"extracted pack `{}` escaped the env directory",
path.display()
))
})?
.to_path_buf();
let pack_id = path
.file_stem()
.and_then(|s| s.to_str())
.map(PackId::new)
.ok_or_else(|| {
OpError::InvalidArgument(format!("pack `{}` has no file stem", path.display()))
})?;
packs.push(LockedPack {
pack_id,
path: rel,
digest,
});
}
packs.sort_by(|a, b| a.path.cmp(&b.path));
let lock = PackListLock {
schema: SchemaVersion::new(SchemaVersion::PACK_LIST_LOCK_V1),
revision_id,
packs,
};
let lock_path = rev_dir.join("pack-list.lock");
atomic_write_json(&lock_path, &lock)
.map_err(|e| OpError::Store(crate::environment::store::StoreError::from(e)))?;
let pack_list_lock_ref = lock_path
.strip_prefix(env_dir)
.map_err(|_| {
OpError::InvalidArgument(format!(
"pack-list.lock `{}` escaped the env directory",
lock_path.display()
))
})?
.to_path_buf();
Ok(StagedBundle {
bundle_digest,
pack_list_lock_ref,
lock,
})
}
fn sha256_file(path: &Path) -> std::io::Result<String> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
}
fn collect_gtpacks(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let file_type = entry.file_type()?;
let path = entry.path();
if file_type.is_dir() {
collect_gtpacks(&path, out)?;
} else if file_type.is_file() && path.extension().and_then(|e| e.to_str()) == Some("gtpack")
{
out.push(path);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn collect_gtpacks_recurses_and_filters_by_extension() {
let dir = tempdir().unwrap();
let root = dir.path();
let dist = root.join("packs/alpha/dist");
std::fs::create_dir_all(&dist).unwrap();
std::fs::write(dist.join("alpha.gtpack"), b"PK\x03\x04").unwrap();
std::fs::write(dist.join("readme.txt"), b"not a pack").unwrap();
std::fs::write(root.join("stray.gtpack"), b"PK\x03\x04").unwrap();
let mut found = Vec::new();
collect_gtpacks(&root.join("packs"), &mut found).unwrap();
assert_eq!(found.len(), 1, "only the packs/ .gtpack, got {found:?}");
assert!(found[0].ends_with("alpha/dist/alpha.gtpack"));
}
}