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}.zwfz"))
36 };
37
38 let file_count = zip_directory(dir, &output_path)?;
39
40 eprintln!(
41 "packed {} files into '{}' (workflow: '{}')",
42 file_count,
43 output_path.display(),
44 wf.workflow.name
45 );
46
47 Ok(output_path)
48}
49
50pub fn zip_directory(dir: &Path, output_path: &Path) -> Result<usize, ZigError> {
56 let files = collect_files(dir)?;
57
58 let file = std::fs::File::create(output_path)
59 .map_err(|e| ZigError::Io(format!("failed to create {}: {e}", output_path.display())))?;
60 let mut zip = zip::ZipWriter::new(file);
61 let options = zip::write::SimpleFileOptions::default()
62 .compression_method(zip::CompressionMethod::Deflated);
63
64 for file_path in &files {
65 let relative = file_path
66 .strip_prefix(dir)
67 .map_err(|e| ZigError::Io(format!("path error: {e}")))?;
68 let name = relative.to_string_lossy().replace('\\', "/");
69
70 zip.start_file(&name, options)
71 .map_err(|e| ZigError::Io(format!("failed to add {name} to archive: {e}")))?;
72
73 let contents = std::fs::read(file_path)
74 .map_err(|e| ZigError::Io(format!("failed to read {}: {e}", file_path.display())))?;
75 zip.write_all(&contents)
76 .map_err(|e| ZigError::Io(format!("failed to write {name} to archive: {e}")))?;
77 }
78
79 zip.finish()
80 .map_err(|e| ZigError::Io(format!("failed to finalize archive: {e}")))?;
81
82 Ok(files.len())
83}
84
85fn find_workflow_toml(dir: &Path) -> Result<PathBuf, ZigError> {
87 let mut candidates = Vec::new();
88
89 for entry in std::fs::read_dir(dir)
90 .map_err(|e| ZigError::Io(format!("failed to read directory {}: {e}", dir.display())))?
91 {
92 let entry =
93 entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
94 let path = entry.path();
95 if path.is_file() {
96 if let Some(ext) = path.extension() {
97 if ext == "toml" || ext == "zwf" {
98 if let Ok(content) = std::fs::read_to_string(&path) {
99 if content.contains("[workflow]") {
100 candidates.push(path);
101 }
102 }
103 }
104 }
105 }
106 }
107
108 match candidates.len() {
109 0 => Err(ZigError::Io(format!(
110 "no workflow TOML file found in '{}'",
111 dir.display()
112 ))),
113 1 => Ok(candidates.into_iter().next().unwrap()),
114 n => Err(ZigError::Io(format!(
115 "found {n} workflow files in '{}' (expected exactly one): {}",
116 dir.display(),
117 candidates
118 .iter()
119 .map(|p| p
120 .file_name()
121 .unwrap_or_default()
122 .to_string_lossy()
123 .to_string())
124 .collect::<Vec<_>>()
125 .join(", ")
126 ))),
127 }
128}
129
130fn collect_files(dir: &Path) -> Result<Vec<PathBuf>, ZigError> {
132 let mut files = Vec::new();
133
134 fn walk(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), ZigError> {
135 for entry in std::fs::read_dir(dir)
136 .map_err(|e| ZigError::Io(format!("failed to read directory: {e}")))?
137 {
138 let entry =
139 entry.map_err(|e| ZigError::Io(format!("failed to read directory entry: {e}")))?;
140 let path = entry.path();
141 if path.is_dir() {
142 walk(&path, files)?;
143 } else {
144 files.push(path);
145 }
146 }
147 Ok(())
148 }
149
150 walk(dir, &mut files)?;
151 files.sort();
152 Ok(files)
153}
154
155#[cfg(test)]
156#[path = "pack_tests.rs"]
157mod tests;