plux_rs/utils/
archive.rs

1use std::{
2    ffi::OsStr,
3    fs::File,
4    io::{Read, Write},
5    path::Path,
6};
7
8use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
9
10use crate::Bundle;
11
12use super::{BundleUnzipError, BundleZipError};
13
14/// Compresses a directory into a ZIP archive.
15///
16/// This function creates a ZIP archive from a directory, preserving the directory structure.
17/// It's commonly used for packaging plugin bundles.
18///
19/// # Parameters
20///
21/// * `path` - Path to the directory to compress
22/// * `target_path` - Directory where the ZIP file will be created
23/// * `compression_method` - Compression method to use (e.g., Stored, Deflated)
24/// * `callback` - Optional callback function called for each processed file/directory
25///
26/// # Returns
27///
28/// Returns `Result<(), BundleZipError>` indicating success or failure.
29///
30/// # Type Parameters
31///
32/// * `S` - Type that can be converted to OsStr (for the source path)
33/// * `F` - Callback function type that takes a `&Path`
34///
35/// # Example
36///
37/// ```rust,no_run
38/// use plux_rs::utils::archive::zip;
39/// use std::path::Path;
40/// use zip::CompressionMethod;
41///
42/// // Compress a plugin directory
43/// zip(
44///     "path/to/plugin_directory",
45///     "output/directory",
46///     CompressionMethod::Deflated,
47///     Some(|path: &Path| println!("Processing: {}", path.display()))
48/// )?;
49/// # Ok::<(), Box<dyn std::error::Error>>(())
50/// ```
51pub fn zip<S, F>(
52    path: &S,
53    target_path: &str,
54    compression_method: zip::CompressionMethod,
55    mut callback: Option<F>,
56) -> Result<(), BundleZipError>
57where
58    S: AsRef<OsStr> + ?Sized,
59    F: FnMut(&Path),
60{
61    let path = Path::new(path);
62    let target_path =
63        Path::new(target_path).join(path.file_name().ok_or(BundleZipError::NoNameFailed)?);
64
65    if !path.is_dir() {
66        return Err(BundleZipError::MissingBundleFailed);
67    }
68
69    match target_path.exists() {
70        true if target_path.is_file() => Ok(()),
71        true => Err(BundleZipError::ContainSameDirFailed),
72        false => Ok({
73            let file =
74                File::create(target_path).map_err(|e| BundleZipError::CreateBundleFailed(e))?;
75            let mut archive = ZipWriter::new(file);
76            let options = SimpleFileOptions::default()
77                .compression_method(compression_method)
78                .unix_permissions(0o755);
79
80            let mut buffer = Vec::new();
81            for entry in walkdir::WalkDir::new(path)
82                .into_iter()
83                .filter_map(|e| e.ok())
84            {
85                let entry_path = entry.path();
86                let name = entry_path.strip_prefix(path).unwrap();
87
88                if entry_path.is_file() {
89                    #[allow(deprecated)]
90                    archive.start_file_from_path(name, options)?;
91                    let mut f = File::open(entry_path)?;
92
93                    f.read_to_end(&mut buffer)?;
94                    archive.write_all(&buffer)?;
95                    buffer.clear();
96                } else if !name.as_os_str().is_empty() {
97                    #[allow(deprecated)]
98                    archive.add_directory_from_path(name, options)?;
99                }
100
101                callback.as_mut().map(|callback| callback(name));
102            }
103        }),
104    }
105}
106
107/// Extracts a ZIP archive to a directory.
108///
109/// This function extracts a ZIP archive and creates a Bundle from the extracted directory.
110/// It's commonly used for unpacking plugin bundles.
111///
112/// # Parameters
113///
114/// * `path` - Path to the ZIP file to extract
115/// * `target_path` - Directory where the archive will be extracted
116///
117/// # Returns
118///
119/// Returns `Result<Bundle, BundleUnzipError>` containing the bundle information
120/// from the extracted directory on success.
121///
122/// # Type Parameters
123///
124/// * `S` - Type that can be converted to OsStr (for the archive path)
125///
126/// # Example
127///
128/// ```rust,no_run
129/// use plux_rs::utils::archive::unzip;
130///
131/// // Extract a plugin bundle
132/// let bundle = unzip("path/to/plugin.zip", "output/directory")?;
133/// println!("Extracted plugin: {}", bundle.id);
134/// # Ok::<(), Box<dyn std::error::Error>>(())
135/// ```
136pub fn unzip<S>(path: &S, target_path: &str) -> Result<Bundle, BundleUnzipError>
137where
138    S: AsRef<OsStr> + ?Sized,
139{
140    let path = Path::new(path);
141    let target_path =
142        Path::new(target_path).join(path.file_name().ok_or(BundleUnzipError::NoNameFailed)?);
143
144    if !path.is_file() {
145        return Err(BundleUnzipError::MissingBundleFailed);
146    }
147
148    match target_path.exists() {
149        true if target_path.is_dir() => Ok(()),
150        true => Err(BundleUnzipError::ContainSameFileFailed),
151        false => Ok({
152            let file = File::open(path)?;
153            let mut archive = ZipArchive::new(file)?;
154            archive.extract(&target_path)?;
155        }),
156    }?;
157
158    Ok(Bundle::from_filename(target_path.file_name().unwrap())?)
159}
160
161#[test]
162fn test_zip() {
163    let temp_path = "./tests/bundles/temp";
164    if !Path::new(temp_path).exists() {
165        std::fs::create_dir_all(temp_path).unwrap();
166    }
167
168    let name = "plugin_a-v1.0.0.vpl";
169    let path = format!("./tests/bundles/{name}");
170
171    let target_path = temp_path;
172    zip(
173        &path,
174        target_path,
175        zip::CompressionMethod::Stored,
176        Some(|name: &Path| println!("{}", name.display())),
177    )
178    .unwrap();
179
180    std::fs::remove_file(format!("{target_path}/{name}")).unwrap();
181}
182
183#[test]
184fn test_unzip() {
185    let temp_path = "./tests/bundles/temp";
186    if !Path::new(temp_path).exists() {
187        std::fs::create_dir_all(temp_path).unwrap();
188    }
189
190    let name = "plugin_b-v1.0.0.vpl";
191    let path = format!("./tests/bundles/{name}");
192
193    let target_path = temp_path;
194    let bundle = unzip(&path, target_path).unwrap();
195    println!("{bundle}");
196
197    std::fs::remove_dir_all(format!("{target_path}/{name}")).unwrap();
198}