use crate::auxiliary::DirGuard;
use crate::runner::Args;
use std::cmp::max;
use std::fs::{File, read_dir, remove_dir_all, remove_file};
use std::io::{copy, prelude::*, stdout};
use std::path::{Path, PathBuf};
use std::thread;
static S_ARCHIVE: &str = ".tar.zst";
static S_ARCHILIST: &str = "_archived-filelist.txt";
static S_FLAG_MESSAGE: &str = "_archived-message.txt";
static S_TOOL: &str = "zst_";
static RET_TAR_ERROR: u8 = 1;
static RET_ITEM_ERROR: u8 = 2;
static RET_DIR_ERROR: u8 = 3;
pub fn batch_archive(start_dir: PathBuf, args: Args, compress: bool) -> Result<(), u8> {
let _guard = DirGuard::new(&start_dir)?;
let mut ret = 0;
let level_tree = args.leveldir.unwrap_or(4);
match args.input {
None => {
let target_dir = if let Some(target) = &args.target {
Path::new(target)
} else {
start_dir.as_path()
};
match read_dir(&start_dir) {
Ok(entries) => {
let entries: Vec<_> = entries.collect();
let total_items = entries.len();
for (current_item, entry_result) in entries.into_iter().enumerate() {
match entry_result {
Ok(entry) => {
if entry_archive(
entry.path().as_path(),
compress,
args.preserve,
args.flag,
level_tree,
args.zstdlevel,
target_dir,
current_item + 1,
total_items,
) != Ok(())
{
ret = RET_ITEM_ERROR
}
}
Err(e) => {
eprintln!("出错了! Error: {e}");
return Err(RET_DIR_ERROR);
}
}
}
}
Err(e) => eprintln!("出错了! Error: {e}"),
}
}
Some(s) => {
let f_path = Path::new(&s);
let target_dir = if let Some(target) = &args.target {
Path::new(target)
} else {
f_path.parent().ok_or(RET_DIR_ERROR)?
};
if entry_archive(
f_path,
compress,
args.preserve,
args.flag,
level_tree,
args.zstdlevel,
target_dir,
1,
1,
) != Ok(())
{
ret = RET_ITEM_ERROR
}
}
}
match ret {
0 => Ok(()),
_ => Err(ret),
}
}
pub fn entry_archive(
f_path: &Path,
compress: bool,
preserve: bool,
flag: bool,
level_tree: u8,
level_zstd: Option<i32>,
target_dir: &Path,
current: usize,
total: usize,
) -> Result<(), u8> {
let mut ret = 0;
let f_name = f_path
.file_name()
.ok_or(RET_ITEM_ERROR)?
.to_str()
.ok_or(RET_ITEM_ERROR)?;
print!("({current}/{total}) ");
if f_name.find(S_TOOL) == Some(0)
|| (f_name.len() >= S_ARCHILIST.len()
&& f_name.rfind(S_ARCHILIST) == Some(f_name.len() - S_ARCHILIST.len()))
|| (f_name.len() >= S_FLAG_MESSAGE.len()
&& f_name.rfind(S_FLAG_MESSAGE) == Some(f_name.len() - S_FLAG_MESSAGE.len()))
{
println!("Skip: {f_name}");
}
else if f_name.len() >= S_ARCHIVE.len()
&& f_name.rfind(S_ARCHIVE) == Some(f_name.len() - S_ARCHIVE.len())
{
if !compress {
print!("Extract: {:?}", f_path);
let _ = stdout().flush();
let f_ori_name = &f_name[0..f_name.rfind(S_ARCHIVE).unwrap()];
let f_ori_buf = target_dir.join(f_ori_name);
let f_ori = f_ori_buf.as_path();
if do_archive(f_path, target_dir, false, None).is_err() {
eprintln!("出错了! Failed to extract {:?}", f_path);
return Err(RET_TAR_ERROR);
}
println!(" -> {:?}", f_ori);
if !preserve {
let _ = f_remove_print(f_path, false);
let f_list_buf = f_ori.with_file_name(&format!("{f_ori_name}{S_ARCHILIST}"));
let f_list = f_list_buf.as_path();
if Path::exists(f_list) {
let _ = f_remove_print(f_list, false);
}
let f_id_buf = f_ori.with_file_name(&format!("{f_ori_name}{S_FLAG_MESSAGE}"));
let f_id = f_id_buf.as_path();
if Path::exists(f_id) {
let _ = f_remove_print(f_id, false);
}
}
} else {
println!("Skip: {:?}",f_path);
}
}
else if compress {
if f_path.is_dir() {
let f_list_path_buf = target_dir.join(&format!("{f_name}{S_ARCHILIST}"));
let f_list_path = f_list_path_buf.as_path();
if let Err(e) = dir_listing::generate_listing(f_path, f_list_path, level_tree) {
eprintln!(
"出错了! Error generating directory listing for {}: {e}",
f_path.display()
);
ret = RET_ITEM_ERROR;
}
}
print!("Compress: {:?}", f_path);
let _ = stdout().flush();
let f_out = target_dir.join(format!("{f_name}{S_ARCHIVE}"));
if do_archive(f_path, target_dir, true, level_zstd).is_err() {
eprintln!("出错了! Failed to compress {:?}", f_path);
return Err(RET_TAR_ERROR);
}
println!(" -> {:?}", f_out);
if flag {
let f_name_id_buf = f_path.with_file_name(format!("{f_name}{S_FLAG_MESSAGE}"));
let f_name_id = f_name_id_buf.as_path();
let mut f_id = File::create(&f_name_id)
.unwrap_or_else(|_| panic!("出错了! Failed to create file: {:?}", f_name_id));
let message = format!(
"- 这是一则数据整理的消息
- 原数据已经压缩,可能移动到新位置:
{:?}
",
f_out
);
f_id.write_all(message.as_bytes())
.unwrap_or_else(|_| panic!("出错了! Failed to write into file: {:?}", f_name_id));
}
assert!(f_path.exists());
assert!(f_out.is_file());
if !preserve {
let _ = f_remove_print(f_path, f_path.is_dir());
}
} else {
println!("Skip: {:?}", f_path);
}
match ret {
0 => Ok(()),
ret => Err(ret),
}
}
fn do_archive(
f_path: &Path,
target: &Path,
compress: bool,
level_zstd: Option<i32>,
) -> Result<(), u8> {
if compress {
let output_path = target.join(format!(
"{}.tar.zst",
f_path.file_name().unwrap().to_str().unwrap()
));
let output_file = File::create(&output_path).map_err(|_| RET_TAR_ERROR)?;
let (mut reader, writer) = pipe::pipe();
let compressor = thread::spawn(move || {
let mut encoder =
zstd::stream::Encoder::new(output_file, level_zstd.unwrap_or_default()).unwrap();
let cpus = thread::available_parallelism().unwrap().get();
encoder.multithread(max(cpus as u32 / 2, 10)).unwrap();
copy(&mut reader, &mut encoder).unwrap();
encoder.finish().unwrap();
});
{
let mut builder = tar::Builder::new(writer);
if f_path.is_dir() {
builder
.append_dir_all(f_path.file_name().unwrap(), f_path)
.map_err(|_| RET_TAR_ERROR)?;
} else {
builder
.append_path_with_name(f_path, f_path.file_name().unwrap())
.map_err(|_| RET_TAR_ERROR)?;
}
builder.finish().map_err(|_| RET_TAR_ERROR)?;
}
compressor.join().map_err(|_| RET_TAR_ERROR)?;
} else {
let file_stem = f_path.file_stem().unwrap().to_str().unwrap();
let tar_path = target.join(format!("{file_stem}.tar"));
{
let input_file = File::open(f_path).map_err(|_| RET_TAR_ERROR)?;
let output_file = File::create(&tar_path).map_err(|_| RET_TAR_ERROR)?;
zstd::stream::copy_decode(input_file, output_file).map_err(|_| RET_TAR_ERROR)?;
}
let tar_file = File::open(&tar_path).map_err(|_| RET_TAR_ERROR)?;
let mut archive = tar::Archive::new(tar_file);
archive.unpack(target).map_err(|_| RET_TAR_ERROR)?;
std::fs::remove_file(&tar_path).map_err(|_| RET_TAR_ERROR)?;
}
Ok(())
}
mod dir_listing {
use std::fs::{self, DirEntry};
use std::io::{self, Write};
use std::path::Path;
use std::time::SystemTime;
pub fn generate_listing(
dir_path: &Path,
output_path: &Path,
max_depth: u8,
) -> Result<(), io::Error> {
let mut output = fs::File::create(output_path)?;
list_directory(dir_path, &mut output, max_depth, 0)
}
fn list_directory(
path: &Path,
output: &mut fs::File,
max_depth: u8,
current_depth: u8,
) -> io::Result<()> {
if current_depth > max_depth {
return Ok(());
}
let entries = fs::read_dir(path)?;
let mut entries: Vec<DirEntry> = entries.filter_map(Result::ok).collect();
entries.sort_by_key(|a| a.file_name());
for entry in entries {
let metadata = entry.metadata()?;
let file_type = metadata.file_type();
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name.starts_with('.') {
continue;
}
let tree_prefix = if current_depth == 0 {
String::new()
} else {
let mut prefix = String::new();
for i in 0..current_depth {
if i == current_depth - 1 {
prefix.push_str("└──");
} else {
prefix.push_str("│ ");
}
}
prefix
};
let size = if file_type.is_dir() {
let dir_size = dir_size(&entry.path())?;
format!("{:>10}", human_size(dir_size))
} else {
let file_size = metadata.len();
format!("{:>10}", human_size(file_size))
};
let modified = metadata.modified()?;
let modified = system_time_to_date_time(modified);
writeln!(
output,
"{:<19} {:>10} {}{} {}",
modified,
size,
tree_prefix,
if file_type.is_dir() { "┬" } else { "─" },
file_name
)?;
if file_type.is_dir() {
list_directory(&entry.path(), output, max_depth, current_depth + 1)?;
}
}
Ok(())
}
fn dir_size(path: &Path) -> io::Result<u64> {
fn walk_dir(path: &Path, total: &mut u64) -> io::Result<()> {
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_dir() {
walk_dir(&entry.path(), total)?;
} else {
*total += metadata.len();
}
}
Ok(())
}
let mut total = 0;
walk_dir(path, &mut total)?;
Ok(total)
}
fn human_size(size: u64) -> String {
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
let mut size = size as f64;
let mut unit_idx = 0;
while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
size /= 1024.0;
unit_idx += 1;
}
format!("{:.1}{}", size, UNITS[unit_idx])
}
fn system_time_to_date_time(time: SystemTime) -> String {
use chrono::{DateTime, Local};
let datetime: DateTime<Local> = time.into();
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
}
}
fn f_remove_print(f_path: &Path, f_is_dir: bool) -> Result<(), std::io::Error> {
if f_is_dir {
match remove_dir_all(f_path) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!(
"Error, couldn't remove original directory, {:?}: {e}",
f_path
);
Err(e)
}
}
} else {
match remove_file(f_path) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("Error, couldn't remove original file, {:?}: {e}", f_path);
Err(e)
}
}
}
}