Skip to main content

flodl_cli/util/
archive.rs

1//! Archive extraction (zip files).
2//!
3//! Shells out to `unzip` (Linux/macOS) or `PowerShell Expand-Archive` (Windows).
4
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8/// Extract a zip file to `dest_dir`.
9///
10/// Creates `dest_dir` if it doesn't exist.
11pub fn extract_zip(zip_path: &Path, dest_dir: &Path) -> Result<(), String> {
12    std::fs::create_dir_all(dest_dir)
13        .map_err(|e| format!("cannot create {}: {}", dest_dir.display(), e))?;
14
15    let zip_str = zip_path
16        .to_str()
17        .ok_or_else(|| "zip path is not valid UTF-8".to_string())?;
18    let dest_str = dest_dir
19        .to_str()
20        .ok_or_else(|| "destination path is not valid UTF-8".to_string())?;
21
22    let status = if cfg!(target_os = "windows") {
23        Command::new("powershell")
24            .args([
25                "-NoProfile",
26                "-Command",
27                &format!(
28                    "Expand-Archive -Force -Path '{}' -DestinationPath '{}'",
29                    zip_str, dest_str
30                ),
31            ])
32            .stdout(Stdio::null())
33            .stderr(Stdio::inherit())
34            .status()
35    } else {
36        // Check for unzip
37        if Command::new("unzip")
38            .arg("--help")
39            .stdout(Stdio::null())
40            .stderr(Stdio::null())
41            .status()
42            .is_err()
43        {
44            return Err(
45                "unzip is required but not installed.\n\
46                 \n\
47                 \x20 Ubuntu/Debian:  sudo apt install unzip\n\
48                 \x20 Fedora/RHEL:    sudo dnf install unzip\n\
49                 \x20 macOS:          available by default"
50                    .into(),
51            );
52        }
53        Command::new("unzip")
54            .args(["-q", "-o", zip_str, "-d", dest_str])
55            .stdout(Stdio::null())
56            .stderr(Stdio::inherit())
57            .status()
58    };
59
60    match status {
61        Ok(s) if s.success() => Ok(()),
62        Ok(s) => Err(format!(
63            "extraction failed (exit code {})\n  Archive: {}",
64            s.code().unwrap_or(-1),
65            zip_path.display()
66        )),
67        Err(e) => Err(format!("failed to run extraction command: {}", e)),
68    }
69}