use std::fs;
use std::fs::File;
use std::io;
use std::io::BufReader;
use std::io::BufWriter;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
use chrono::Datelike;
use chrono::Timelike;
use zip::result::ZipError;
use zip::result::ZipResult;
use zip::write::FileOptions;
use zip::CompressionMethod;
use zip::ZipWriter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressionOptions {
pub method: CompressionMethod,
pub level: Option<i32>,
}
fn strip_prefix(parent: &Path, child: &Path) -> Result<PathBuf, io::Error> {
match child.strip_prefix(parent) {
Ok(rel_path) => Ok(rel_path.to_path_buf()),
Err(e) => Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Strip prefix error, path: {}, error: {}",
child.to_str().unwrap_or(""),
e
),
)),
}
}
fn path_to_string(path: &Path) -> Result<String, io::Error> {
let st = match path.to_str() {
Some(name) => name.to_string(),
None => {
return Err(io::Error::new(
io::ErrorKind::Other,
"Path access error".to_string(),
))
}
};
let res = st.replace("\\", "/");
Ok(res)
}
fn time_to_zip_time(system_time: &SystemTime) -> zip::DateTime {
let tm: chrono::DateTime<chrono::Utc> = (*system_time).into();
zip::DateTime::from_date_and_time(
tm.year() as u16,
tm.month() as u8,
tm.day() as u8,
tm.hour() as u8,
tm.minute() as u8,
tm.second() as u8,
)
.unwrap_or_default()
}
fn read_dir_paths(dir: &Path) -> Result<Vec<PathBuf>, io::Error> {
let rd = fs::read_dir(dir)?;
let mut res: Vec<PathBuf> = Vec::new();
for en in rd {
let en = en?;
res.push(en.path())
}
res.sort_by(|a, b| {
if a.is_dir() && !b.is_dir() {
std::cmp::Ordering::Less
} else if b.is_dir() && !a.is_dir() {
std::cmp::Ordering::Greater
} else {
a.cmp(b)
}
});
Ok(res)
}
fn zip_file<T: io::Seek + io::Write, F: FnMut(&str)>(
zip: &mut ZipWriter<T>,
root_dir: &Path,
path: &Path,
comp_opts: CompressionOptions,
listener: &mut F,
) -> ZipResult<()> {
let file = File::open(path)?;
let meta = file.metadata()?;
let system_time = meta.modified()?;
let zip_time = time_to_zip_time(&system_time);
let zip64_flag = meta.len() >= (1 << 32);
let options = FileOptions::default()
.compression_method(comp_opts.method)
.compression_level(comp_opts.level)
.large_file(zip64_flag)
.last_modified_time(zip_time);
let rel_path = match root_dir.parent() {
Some(parent) => strip_prefix(parent, path)?,
None => path.to_path_buf(),
};
let name = path_to_string(&rel_path)?;
listener(&name);
zip.start_file(name, options)?;
let mut reader = BufReader::new(file);
std::io::copy(&mut reader, zip)?;
Ok(())
}
fn zip_dir_recursive<T: io::Seek + io::Write, F: FnMut(&str)>(
zip: &mut ZipWriter<T>,
root_dir: &Path,
dir: &Path,
comp_opts: CompressionOptions,
listener: &mut F,
) -> ZipResult<()> {
if !dir.is_dir() {
return Err(ZipError::FileNotFound);
}
let rel_path = match root_dir.parent() {
Some(parent) => strip_prefix(parent, dir)?,
None => dir.to_path_buf(),
};
let name = path_to_string(&rel_path)?;
listener(&format!("{}/", &name));
let medatata = dir.metadata()?;
let system_time = medatata.modified()?;
let zip_time = time_to_zip_time(&system_time);
let options = FileOptions::default().last_modified_time(zip_time);
zip.add_directory(name, options)?;
for path in read_dir_paths(dir)? {
if path.is_dir() {
zip_dir_recursive(zip, root_dir, &path, comp_opts, listener)?;
} else {
zip_file(zip, root_dir, &path, comp_opts, listener)?;
}
}
Ok(())
}
pub fn zip_directory_listen<P: AsRef<Path>, F: FnMut(&str)>(
src_dir: P,
dst_file: P,
comp_opts: CompressionOptions,
mut listener: F,
) -> ZipResult<()> {
let src_dir_path = src_dir.as_ref();
if !src_dir_path.is_dir() {
return Err(ZipError::FileNotFound);
}
let zip_file = match File::create(dst_file.as_ref()) {
Ok(file) => file,
Err(e) => return Err(ZipError::Io(e)),
};
let mut zip = zip::ZipWriter::new(BufWriter::new(zip_file));
zip_dir_recursive(
&mut zip,
src_dir_path,
src_dir_path,
comp_opts,
&mut listener,
)?;
Ok(())
}
pub fn zip_directory<P: AsRef<Path>>(
src_dir: P,
dst_file: P,
comp_opts: CompressionOptions,
) -> ZipResult<()> {
zip_directory_listen(src_dir, dst_file, comp_opts, |_| {})
}
pub fn unzip_directory_listen<P: AsRef<Path>, F: FnMut(&str)>(
zip_file: P,
dest_dir: P,
mut listener: F,
) -> ZipResult<String> {
let file = match File::open(zip_file) {
Ok(file) => file,
Err(e) => return Err(ZipError::Io(e)),
};
let mut zip = zip::ZipArchive::new(BufReader::new(file))?;
for i in 0..zip.len() {
let file = zip.by_index(i)?;
listener(file.name());
let filepath = file
.enclosed_name()
.ok_or(ZipError::InvalidArchive("Invalid file path"))?;
let outpath = dest_dir.as_ref().join(filepath);
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let outfile = fs::File::create(&outpath)?;
let mut reader = BufReader::new(file);
let mut writer = BufWriter::new(outfile);
io::copy(&mut reader, &mut writer)?;
}
}
let entry = zip.by_index(0)?;
Ok(entry.name().to_string())
}
pub fn unzip_directory<P: AsRef<Path>>(zip_file: P, dest_dir: P) -> ZipResult<String> {
unzip_directory_listen(zip_file, dest_dir, |_| {})
}