zip_recurse/
lib.rs

1/*
2 * Copyright 2024, WiltonDB Software
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::fs;
18use std::fs::File;
19use std::io;
20use std::io::BufReader;
21use std::io::BufWriter;
22use std::path::Path;
23use std::path::PathBuf;
24use std::time::SystemTime;
25
26use chrono::Datelike;
27use chrono::Timelike;
28use zip::result::ZipError;
29use zip::result::ZipResult;
30use zip::write::FileOptions;
31use zip::CompressionMethod;
32use zip::ZipWriter;
33
34/// Compression method & level according to [`zip::CompressionMethod`]
35/// and [`zip::write::FileOptions::compression_level`].
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct CompressionOptions {
38    pub method: CompressionMethod,
39    pub level: Option<i32>,
40}
41
42fn strip_prefix(parent: &Path, child: &Path) -> Result<PathBuf, io::Error> {
43    match child.strip_prefix(parent) {
44        Ok(rel_path) => Ok(rel_path.to_path_buf()),
45        Err(e) => Err(io::Error::new(
46            io::ErrorKind::Other,
47            format!(
48                "Strip prefix error, path: {}, error: {}",
49                child.to_str().unwrap_or(""),
50                e
51            ),
52        )),
53    }
54}
55
56fn path_to_string(path: &Path) -> Result<String, io::Error> {
57    let st = match path.to_str() {
58        Some(name) => name.to_string(),
59        None => {
60            return Err(io::Error::new(
61                io::ErrorKind::Other,
62                "Path access error".to_string(),
63            ))
64        }
65    };
66    let res = st.replace("\\", "/");
67    Ok(res)
68}
69
70fn time_to_zip_time(system_time: &SystemTime) -> zip::DateTime {
71    let tm: chrono::DateTime<chrono::Utc> = (*system_time).into();
72    zip::DateTime::from_date_and_time(
73        tm.year() as u16,
74        tm.month() as u8,
75        tm.day() as u8,
76        tm.hour() as u8,
77        tm.minute() as u8,
78        tm.second() as u8,
79    )
80    .unwrap_or_default()
81}
82
83fn read_dir_paths(dir: &Path) -> Result<Vec<PathBuf>, io::Error> {
84    let rd = fs::read_dir(dir)?;
85    let mut res: Vec<PathBuf> = Vec::new();
86    for en in rd {
87        let en = en?;
88        res.push(en.path())
89    }
90    res.sort_by(|a, b| {
91        if a.is_dir() && !b.is_dir() {
92            std::cmp::Ordering::Less
93        } else if b.is_dir() && !a.is_dir() {
94            std::cmp::Ordering::Greater
95        } else {
96            a.cmp(b)
97        }
98    });
99    Ok(res)
100}
101
102fn zip_file<T: io::Seek + io::Write, F: FnMut(&str)>(
103    zip: &mut ZipWriter<T>,
104    root_dir: &Path,
105    path: &Path,
106    comp_opts: CompressionOptions,
107    listener: &mut F,
108) -> ZipResult<()> {
109    let file = File::open(path)?;
110    let meta = file.metadata()?;
111    let system_time = meta.modified()?;
112    let zip_time = time_to_zip_time(&system_time);
113    let zip64_flag = meta.len() >= (1 << 32);
114    let options = FileOptions::default()
115        .compression_method(comp_opts.method)
116        .compression_level(comp_opts.level)
117        .large_file(zip64_flag)
118        .last_modified_time(zip_time);
119
120    let rel_path = match root_dir.parent() {
121        Some(parent) => strip_prefix(parent, path)?,
122        None => path.to_path_buf(),
123    };
124    let name = path_to_string(&rel_path)?;
125    listener(&name);
126    zip.start_file(name, options)?;
127    let mut reader = BufReader::new(file);
128    std::io::copy(&mut reader, zip)?;
129    Ok(())
130}
131
132fn zip_dir_recursive<T: io::Seek + io::Write, F: FnMut(&str)>(
133    zip: &mut ZipWriter<T>,
134    root_dir: &Path,
135    dir: &Path,
136    comp_opts: CompressionOptions,
137    listener: &mut F,
138) -> ZipResult<()> {
139    if !dir.is_dir() {
140        return Err(ZipError::FileNotFound);
141    }
142    let rel_path = match root_dir.parent() {
143        Some(parent) => strip_prefix(parent, dir)?,
144        None => dir.to_path_buf(),
145    };
146    let name = path_to_string(&rel_path)?;
147    listener(&format!("{}/", &name));
148    let medatata = dir.metadata()?;
149    let system_time = medatata.modified()?;
150    let zip_time = time_to_zip_time(&system_time);
151    let options = FileOptions::default().last_modified_time(zip_time);
152    zip.add_directory(name, options)?;
153    for path in read_dir_paths(dir)? {
154        if path.is_dir() {
155            zip_dir_recursive(zip, root_dir, &path, comp_opts, listener)?;
156        } else {
157            zip_file(zip, root_dir, &path, comp_opts, listener)?;
158        }
159    }
160    Ok(())
161}
162
163/// Compresses directory as a ZIP archive
164///
165/// Recursively compresses specified directory as a ZIP archive.
166/// Listener is called for each entry added to archive.
167///
168/// # Arguments
169///
170/// * `src_dir` - Path to directory to compress
171/// * `dst_file` - Path to resulting ZIP file
172/// * `comp_opts` - Compression method and level
173/// * `listener` - Function that is called for each entry added to archive
174pub fn zip_directory_listen<P: AsRef<Path>, F: FnMut(&str)>(
175    src_dir: P,
176    dst_file: P,
177    comp_opts: CompressionOptions,
178    mut listener: F,
179) -> ZipResult<()> {
180    let src_dir_path = src_dir.as_ref();
181    if !src_dir_path.is_dir() {
182        return Err(ZipError::FileNotFound);
183    }
184    let zip_file = match File::create(dst_file.as_ref()) {
185        Ok(file) => file,
186        Err(e) => return Err(ZipError::Io(e)),
187    };
188    let mut zip = zip::ZipWriter::new(BufWriter::new(zip_file));
189    zip_dir_recursive(
190        &mut zip,
191        src_dir_path,
192        src_dir_path,
193        comp_opts,
194        &mut listener,
195    )?;
196    Ok(())
197}
198
199/// Compresses directory as a ZIP archive
200///
201/// Recursively compresses specified directory as a ZIP archive.
202///
203/// # Arguments
204///
205/// * `src_dir` - Path to directory to compress
206/// * `dst_file` - Path to resulting ZIP file
207/// * `comp_opts` - Compression method and level
208pub fn zip_directory<P: AsRef<Path>>(
209    src_dir: P,
210    dst_file: P,
211    comp_opts: CompressionOptions,
212) -> ZipResult<()> {
213    zip_directory_listen(src_dir, dst_file, comp_opts, |_| {})
214}
215
216/// Unpack ZIP archive
217///
218/// Unpacks specified ZIP archive into a directory.
219///
220/// # Arguments
221///
222/// * `zip_file` - Path to ZIP file to unpack
223/// * `dest_dir` - Destination directory
224/// * `listener` - Function that is called for each entry read from archive
225pub fn unzip_directory_listen<P: AsRef<Path>, F: FnMut(&str)>(
226    zip_file: P,
227    dest_dir: P,
228    mut listener: F,
229) -> ZipResult<String> {
230    let file = match File::open(zip_file) {
231        Ok(file) => file,
232        Err(e) => return Err(ZipError::Io(e)),
233    };
234    let mut zip = zip::ZipArchive::new(BufReader::new(file))?;
235    for i in 0..zip.len() {
236        let file = zip.by_index(i)?;
237        listener(file.name());
238        let filepath = file
239            .enclosed_name()
240            .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
241        let outpath = dest_dir.as_ref().join(filepath);
242        if file.name().ends_with('/') {
243            fs::create_dir_all(&outpath)?;
244        } else {
245            if let Some(p) = outpath.parent() {
246                if !p.exists() {
247                    fs::create_dir_all(p)?;
248                }
249            }
250            let outfile = fs::File::create(&outpath)?;
251            let mut reader = BufReader::new(file);
252            let mut writer = BufWriter::new(outfile);
253            io::copy(&mut reader, &mut writer)?;
254        }
255    }
256    let entry = zip.by_index(0)?;
257    Ok(entry.name().to_string())
258}
259
260/// Unpack ZIP archive
261///
262/// Unpacks specified ZIP archive into a directory.
263///
264/// # Arguments
265///
266/// * `zip_file` - Path to ZIP file to unpack
267/// * `dest_dir` - Destination directory
268pub fn unzip_directory<P: AsRef<Path>>(zip_file: P, dest_dir: P) -> ZipResult<String> {
269    unzip_directory_listen(zip_file, dest_dir, |_| {})
270}