1use 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#[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
163pub 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
199pub 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
216pub 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
260pub fn unzip_directory<P: AsRef<Path>>(zip_file: P, dest_dir: P) -> ZipResult<String> {
269 unzip_directory_listen(zip_file, dest_dir, |_| {})
270}