use crate::error::HbackupError;
use crate::job::CompressFormat;
use crate::job::Level;
use anyhow::bail;
use anyhow::{Context, Result};
use bzip2::Compression as BzCompression;
use bzip2::write::BzEncoder;
use flate2::{Compression, write::GzEncoder};
use lz4::EncoderBuilder as Lz4EncoderBuilder;
use sevenz_rust2::ArchiveWriter;
use sevenz_rust2::encoder_options::Lzma2Options;
use std::io::{BufReader, Read, Write};
use std::path::PathBuf;
use std::{fs, io};
use std::{fs::File, path::Path};
use tar::Builder;
use walkdir::WalkDir;
use xz2::write::XzEncoder;
use zip::{ZipWriter, write::FileOptions};
use zstd::stream::write::Encoder as ZstdEncoder;
pub fn copy(src: &Path, dest: &Path) -> Result<()> {
if create_dir(src, dest)? {
return Ok(());
}
let dest = if dest.is_dir() {
let file_name = src.file_name().with_context(|| "Invalid file name")?;
dest.join(file_name)
} else {
dest.into()
};
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
if let Err(e) = fs::copy(src, &dest) {
if e.kind() == io::ErrorKind::PermissionDenied {
eprintln!(
"permission denied: try `chmod u+w '{}'` or remove the destination file before copying",
dest.display()
);
}
bail!(e);
}
Ok(())
}
pub async fn copy_async(src: PathBuf, dest: PathBuf) -> Result<()> {
if create_dir(&src, &dest)? {
return Ok(());
}
let dest = if dest.is_dir() {
let file_name = src.file_name().with_context(|| "Invalid file name")?;
dest.join(file_name)
} else {
dest
};
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
if let Err(e) = tokio::fs::copy(&src, &dest).await {
if e.kind() == io::ErrorKind::PermissionDenied {
eprintln!(
"permission denied: try `chmod u+w '{}'` or remove the destination file before copying",
dest.display()
);
}
bail!(e);
}
Ok(())
}
fn create_dir(src: &Path, dest: &Path) -> Result<bool> {
if !src.exists() {
bail!("The path {src:?} does not exist");
} else if src.is_dir() {
return if dest.is_file() {
bail!("Cannot copy directory {src:?} to file {dest:?}");
} else {
fs::create_dir_all(dest)?;
Ok(true)
};
}
Ok(false)
}
pub fn compression(
src: &Path,
dest: &Path,
format: &CompressFormat,
level: &Level,
ignore: Option<&[String]>,
) -> Result<()> {
if !src.exists() {
bail!(HbackupError::PathNotFound(src.to_path_buf()));
}
if !(src.is_file() || src.is_dir()) {
bail!("compression only supports files and directories");
}
if dest.exists() && !dest.is_dir() {
bail!("destination must be a directory");
}
fs::create_dir_all(dest)?;
match format {
CompressFormat::Gzip => compress_gzip(src, dest, level, ignore),
CompressFormat::Zip => compress_zip(src, dest, level, ignore),
CompressFormat::Sevenz => compress_sevenz(src, dest, level, ignore),
CompressFormat::Zstd => compress_zstd(src, dest, level, ignore),
CompressFormat::Bzip2 => compress_bzip2(src, dest, level, ignore),
CompressFormat::Xz => compress_xz(src, dest, level, ignore),
CompressFormat::Lz4 => compress_lz4(src, dest, level, ignore),
CompressFormat::Tar => compress_tar(src, dest, ignore),
}
}
fn compress_gzip(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let level = match level {
Level::Fastest => Compression::fast(),
Level::Faster => Compression::new(3),
Level::Default => Compression::default(),
Level::Better => Compression::new(8),
Level::Best => Compression::best(),
};
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar.gz"));
let tar_gz = File::create(dest)?;
let encoder = GzEncoder::new(tar_gz, level);
let mut tar_builder = tar::Builder::new(encoder);
append_regular_only(&mut tar_builder, src, ignore)?;
tar_builder.into_inner()?.finish()?;
} else {
let dest = dest.join(format!("{file_name}.gz"));
let dest_file = File::create(&dest)?;
let mut reader = BufReader::new(File::open(src)?);
let mut encoder = GzEncoder::new(dest_file, level);
io::copy(&mut reader, &mut encoder)?;
encoder.finish()?;
}
Ok(())
}
fn compress_zip(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let dest = dest.join(format!("{file_name}.zip"));
let dest_file = File::create(dest)?;
let mut zip = ZipWriter::new(dest_file);
let level = match level {
Level::Fastest => 1,
Level::Faster => 3,
Level::Default => 6,
Level::Better => 8,
Level::Best => 9,
};
let options = FileOptions::<()>::default().compression_level(Some(level));
if src.is_dir() {
let prefix = src.parent().unwrap_or_else(|| Path::new(""));
let ignore_path = match ignore {
Some(ignore) => ignore.iter().map(|s| src.join(s)).collect::<Vec<PathBuf>>(),
None => vec![],
};
for entry in WalkDir::new(src) {
let entry = entry?;
let path = entry.path();
if ignore_path.iter().any(|p| path.starts_with(p)) {
continue;
}
let name = path
.strip_prefix(prefix)
.unwrap()
.to_string_lossy()
.into_owned();
let md = fs::symlink_metadata(path)?;
if md.is_dir() {
zip.add_directory(name, options)?;
} else if md.is_file() {
zip.start_file(name, options)?;
let mut f = File::open(path)?;
io::copy(&mut f, &mut zip)?;
}
}
} else {
zip.start_file(file_name, options)?;
let mut src_file = File::open(src)?;
let mut buffer = Vec::new();
src_file.read_to_end(&mut buffer)?;
zip.write_all(&buffer)?;
zip.finish()?;
}
Ok(())
}
fn compress_sevenz(
src: &Path,
dest: &Path,
level: &Level,
ignore: Option<&[String]>,
) -> Result<()> {
let file_name = get_file_name(src);
let dest = dest.join(format!("{file_name}.7z"));
let mut writer = ArchiveWriter::create(dest)?;
let level = match level {
Level::Fastest => 1,
Level::Faster => 3,
Level::Default => 6,
Level::Better => 8,
Level::Best => 9,
};
let lzma2 = Lzma2Options::from_level(level).into();
writer.set_content_methods(vec![lzma2]);
writer.push_source_path(src, make_filter(src, ignore))?;
writer.finish()?;
Ok(())
}
fn compress_zstd(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let level = match level {
Level::Fastest => 1,
Level::Faster => 2,
Level::Default => 3,
Level::Better => 19,
Level::Best => 22,
};
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar.zst"));
let tar_zst = File::create(dest)?;
let encoder = ZstdEncoder::new(tar_zst, level)?;
let mut tar_builder = tar::Builder::new(encoder);
append_regular_only(&mut tar_builder, src, ignore)?;
tar_builder.into_inner()?.finish()?;
} else {
let dest = dest.join(format!("{file_name}.zst"));
let dest_file = File::create(dest)?;
let mut reader = BufReader::new(File::open(src)?);
let mut encoder = ZstdEncoder::new(dest_file, level)?;
io::copy(&mut reader, &mut encoder)?;
encoder.finish()?;
}
Ok(())
}
fn compress_bzip2(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let level = match level {
Level::Fastest => BzCompression::fast(),
Level::Faster => BzCompression::new(3),
Level::Default => BzCompression::default(),
Level::Better => BzCompression::new(8),
Level::Best => BzCompression::best(),
};
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar.bz2"));
let tar_bz = File::create(dest)?;
let encoder = BzEncoder::new(tar_bz, level);
let mut tar_builder = tar::Builder::new(encoder);
append_regular_only(&mut tar_builder, src, ignore)?;
tar_builder.into_inner()?.finish()?;
} else {
let dest = dest.join(format!("{file_name}.bz2"));
let dest_file = File::create(dest)?;
let mut reader = BufReader::new(File::open(src)?);
let mut encoder = BzEncoder::new(dest_file, level);
io::copy(&mut reader, &mut encoder)?;
encoder.finish()?;
}
Ok(())
}
fn compress_xz(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let level = match level {
Level::Fastest => 1,
Level::Faster => 3,
Level::Default => 6,
Level::Better => 8,
Level::Best => 9,
};
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar.xz"));
let tar_xz = File::create(dest)?;
let encoder = XzEncoder::new(tar_xz, level);
let mut tar_builder = tar::Builder::new(encoder);
append_regular_only(&mut tar_builder, src, ignore)?;
tar_builder.into_inner()?.finish()?;
} else {
let dest = dest.join(format!("{file_name}.xz"));
let dest_file = File::create(dest)?;
let mut reader = BufReader::new(File::open(src)?);
let mut encoder = XzEncoder::new(dest_file, level);
io::copy(&mut reader, &mut encoder)?;
encoder.finish()?;
}
Ok(())
}
fn compress_lz4(src: &Path, dest: &Path, level: &Level, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
let level = match level {
Level::Fastest => 1,
Level::Faster => 3,
Level::Default => 6,
Level::Better => 14,
Level::Best => 16,
};
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar.lz4"));
let tar_lz = File::create(dest)?;
let encoder = Lz4EncoderBuilder::new().level(level).build(tar_lz)?;
let mut tar_builder = tar::Builder::new(encoder);
append_regular_only(&mut tar_builder, src, ignore)?;
let (_, result) = tar_builder.into_inner()?.finish();
result?;
} else {
let dest = dest.join(format!("{file_name}.lz4"));
let dest_file = File::create(dest)?;
let mut reader = BufReader::new(File::open(src)?);
let mut encoder = Lz4EncoderBuilder::new().level(level).build(dest_file)?;
io::copy(&mut reader, &mut encoder)?;
let (_, result) = encoder.finish();
result?;
}
Ok(())
}
fn get_file_name(file: &Path) -> String {
file.file_name().unwrap().to_string_lossy().into_owned()
}
fn append_regular_only<W: Write>(
tar: &mut Builder<W>,
src: &Path,
ignore: Option<&[String]>,
) -> Result<()> {
let prefix = src.parent().unwrap_or(Path::new(""));
let ignore_paths: Vec<PathBuf> = ignore
.map(|dirs| dirs.iter().map(|s| src.join(s)).collect())
.unwrap_or_default();
for entry in WalkDir::new(src) {
let entry = entry?;
let path = entry.path();
if ignore_paths.iter().any(|p| path.starts_with(p)) {
continue;
}
let rel = path.strip_prefix(prefix).unwrap();
let md = fs::symlink_metadata(path)?;
if md.is_dir() {
tar.append_dir(rel, path)?;
} else if md.is_file() {
tar.append_path_with_name(path, rel)?;
}
}
Ok(())
}
fn make_filter(base: &Path, ignore: Option<&[String]>) -> impl Fn(&Path) -> bool {
let ignore_paths: Vec<PathBuf> = ignore
.map(|dirs| dirs.iter().map(|s| base.join(s)).collect())
.unwrap_or_default();
move |path| !ignore_paths.iter().any(|p| path.starts_with(p))
}
fn compress_tar(src: &Path, dest: &Path, ignore: Option<&[String]>) -> Result<()> {
let file_name = get_file_name(src);
if src.is_dir() {
let dest = dest.join(format!("{file_name}.tar"));
let tar_file = File::create(dest)?;
let mut tar_builder = tar::Builder::new(tar_file);
append_regular_only(&mut tar_builder, src, ignore)?;
tar_builder.into_inner()?;
} else {
let dest = dest.join(format!("{file_name}.tar"));
let tar_file = File::create(dest)?;
let mut tar_builder = tar::Builder::new(tar_file);
tar_builder.append_path_with_name(src, file_name)?;
tar_builder.into_inner()?;
}
Ok(())
}