1use std::io::Write;
2use std::path::{Path, PathBuf};
3
4use crate::error::ZigError;
5use crate::workflow::parser;
6
7pub fn pack(dir_path: &str, output: Option<&str>) -> Result<PathBuf, ZigError> {
14 let dir = Path::new(dir_path);
15 if !dir.is_dir() {
16 return Err(ZigError::Io(format!(
17 "'{}' is not a directory",
18 dir.display()
19 )));
20 }
21
22 let toml_file = find_workflow_toml(dir)?;
24
25 let content = std::fs::read_to_string(&toml_file)
27 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", toml_file.display())))?;
28 let wf = parser::parse(&content)?;
29
30 let output_path = if let Some(out) = output {
32 PathBuf::from(out)
33 } else {
34 let name = wf.workflow.name.replace(' ', "-").to_lowercase();
35 PathBuf::from(format!("{name}.zug"))
36 };
37
38 let files = collect_files(dir)?;
40
41 let file = std::fs::File::create(&output_path)
43 .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", output_path.display())))?;
44 let mut zip = zip::ZipWriter::new(file);
45 let options = zip::write::SimpleFileOptions::default()
46 .compression_method(zip::CompressionMethod::Deflated);
47
48 for file_path in &files {
49 let relative = file_path
50 .strip_prefix(dir)
51 .map_err(|e| ZigError::Io(format!("path error: {e}")))?;
52 let name = relative.to_string_lossy().replace('\\', "/");
53
54 zip.start_file(&name, options)
55 .map_err(|e| ZigError::Io(format!("failed to add {name} to archive: {e}")))?;
56
57 let contents = std::fs::read(file_path)
58 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", file_path.display())))?;
59 zip.write_all(&contents)
60 .map_err(|e| ZigError::Io(format!("failed to write {name} to archive: {e}")))?;
61 }
62
63 zip.finish()
64 .map_err(|e| ZigError::Io(format!("failed to finalize archive: {e}")))?;
65
66 eprintln!(
67 "packed {} files into '{}' (workflow: '{}')",
68 files.len(),
69 output_path.display(),
70 wf.workflow.name
71 );
72
73 Ok(output_path)
74}
75
76fn find_workflow_toml(dir: &Path) -> Result<PathBuf, ZigError> {
78 let mut candidates = Vec::new();
79
80 for entry in std::fs::read_dir(dir)
81 .map_err(|e| ZigError::Io(format!("failed to read directory {}: {e}", dir.display())))?
82 {
83 let entry =
84 entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
85 let path = entry.path();
86 if path.is_file() {
87 if let Some(ext) = path.extension() {
88 if ext == "toml" || ext == "zug" {
89 if let Ok(content) = std::fs::read_to_string(&path) {
90 if content.contains("[workflow]") {
91 candidates.push(path);
92 }
93 }
94 }
95 }
96 }
97 }
98
99 match candidates.len() {
100 0 => Err(ZigError::Io(format!(
101 "no workflow TOML file found in '{}'",
102 dir.display()
103 ))),
104 1 => Ok(candidates.into_iter().next().unwrap()),
105 n => Err(ZigError::Io(format!(
106 "found {n} workflow files in '{}' (expected exactly one): {}",
107 dir.display(),
108 candidates
109 .iter()
110 .map(|p| p
111 .file_name()
112 .unwrap_or_default()
113 .to_string_lossy()
114 .to_string())
115 .collect::<Vec<_>>()
116 .join(", ")
117 ))),
118 }
119}
120
121fn collect_files(dir: &Path) -> Result<Vec<PathBuf>, ZigError> {
123 let mut files = Vec::new();
124
125 fn walk(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), ZigError> {
126 for entry in std::fs::read_dir(dir)
127 .map_err(|e| ZigError::Io(format!("failed to read directory: {e}")))?
128 {
129 let entry =
130 entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
131 let path = entry.path();
132 if path.is_dir() {
133 walk(&path, files)?;
134 } else {
135 files.push(path);
136 }
137 }
138 Ok(())
139 }
140
141 walk(dir, &mut files)?;
142 files.sort();
143 Ok(files)
144}
145
146#[cfg(test)]
147#[path = "pack_tests.rs"]
148mod tests;