use ignore::WalkBuilder;
use sha1::{Digest, Sha1};
use std::{
io::Write,
path::{Path, PathBuf},
};
use tracing::debug;
use zip::{ZipWriter, write::SimpleFileOptions};
use crate::{schema::packages::PackageUri, utils::constants::RAY_PKG_PREFIX};
#[derive(Clone, Debug)]
pub struct DirectoryPackage {
uri: PackageUri,
data: Vec<u8>,
}
impl DirectoryPackage {
pub fn new(directory: &Path) -> crate::Result<Self> {
debug!("Creating package from {:?}", directory);
let mut hasher = Sha1::new();
let mut zip = ZipWriter::new(std::io::Cursor::new(Vec::new()));
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for rel_path in Self::walk_files(directory)? {
debug!("Adding file to package: {:?}", rel_path);
let file_data = std::fs::read(directory.join(&rel_path))?;
hasher.update(rel_path.to_string_lossy().as_bytes());
hasher.update(&file_data);
zip.start_file(rel_path.to_string_lossy(), options)?;
zip.write_all(&file_data)?;
}
let package_data = zip.finish()?.into_inner();
let hash = format!("{:x}", hasher.finalize());
let package_name = format!("{}{}.zip", RAY_PKG_PREFIX, hash);
Ok(Self {
uri: PackageUri::new(package_name),
data: package_data,
})
}
pub fn uri(&self) -> &PackageUri {
&self.uri
}
pub fn data(&self) -> &[u8] {
&self.data
}
fn walk_files(directory: &Path) -> crate::Result<Vec<PathBuf>> {
let walker = WalkBuilder::new(directory)
.standard_filters(true)
.git_ignore(true)
.build();
let mut rel_paths = Vec::new();
for result in walker {
let entry =
result.map_err(|e| crate::Error::Generic(format!("Failed to walk directory: {e}")))?;
if entry.file_type().is_some_and(|ft| ft.is_file()) {
let rel_path = entry
.path()
.strip_prefix(directory)
.map_err(|e| crate::Error::Generic(format!("Failed to compute relative path: {e}")))?;
rel_paths.push(rel_path.to_path_buf());
}
}
rel_paths.sort();
Ok(rel_paths)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_hash_directory_deterministic() {
let temp_dir = tempfile::tempdir().unwrap();
fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(temp_dir.path().join("file2.txt"), "content2").unwrap();
let pkg1 = DirectoryPackage::new(temp_dir.path()).unwrap();
let pkg2 = DirectoryPackage::new(temp_dir.path()).unwrap();
assert_eq!(pkg1.uri().name, pkg2.uri().name);
}
#[test]
fn test_create_package() {
let temp_dir = tempfile::tempdir().unwrap();
fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(temp_dir.path().join("file2.txt"), "content2").unwrap();
let package = DirectoryPackage::new(temp_dir.path()).unwrap();
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(package.data)).unwrap();
let mut names: Vec<String> = (0..archive.len())
.map(|i| archive.by_index(i).unwrap().name().to_string())
.collect();
names.sort();
assert_eq!(names, vec!["file1.txt", "file2.txt"]);
}
#[test]
fn test_package_name_and_uri() {
let temp_dir = tempfile::tempdir().unwrap();
fs::write(temp_dir.path().join("file.txt"), "content").unwrap();
let pkg = DirectoryPackage::new(temp_dir.path()).unwrap();
assert!(pkg.uri().name.starts_with(RAY_PKG_PREFIX));
assert!(pkg.uri().name.ends_with(".zip"));
}
}