Skip to main content

zig_core/
pack.rs

1use std::io::Write;
2use std::path::{Path, PathBuf};
3
4use crate::error::ZigError;
5use crate::workflow::parser;
6
7/// Pack a workflow directory into a `.zwfz` zip archive.
8///
9/// The directory must contain exactly one workflow TOML file (`.toml` or `.zwf`
10/// with a `[workflow]` section). All files in the directory are included
11/// in the archive, preserving directory structure. The resulting zip file
12/// can be used directly with `zig run` and `zig validate`.
13pub 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    // Find the workflow TOML file
23    let toml_file = find_workflow_toml(dir)?;
24
25    // Validate it parses correctly
26    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    // Determine output path
31    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
50/// Zip every file under `dir` into a new archive at `output_path`.
51///
52/// Preserves directory structure relative to `dir`. Returns the number of
53/// files written. Used by [`pack`] and by `update::run_update` to re-zip
54/// a staging directory after an interactive edit session.
55pub 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
85/// Find the single workflow TOML file in a directory.
86fn 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
130/// Recursively collect all files in a directory.
131fn 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;