1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
use std::ffi::OsStr;
use std::fs::remove_file;
use std::process::{Command, Stdio};
use anyhow::anyhow;
use bczhc_lib::str::GenericOsStrExt;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use crate::cli::DedupeArgs;
use crate::group::collect_and_group_files;
use crate::{parse_input_file, print_redundant_size};
macro_rules! os_str {
($s:expr) => {
OsStr::new($s)
};
}
pub fn main(args: DedupeArgs) -> anyhow::Result<()> {
let groups = match args.common.input_file {
None => collect_and_group_files(&args.common)?,
Some(f) => parse_input_file(&f)?,
};
print_redundant_size(&groups);
let operation_count = groups.iter().map(|x| x.files.len() as u64 - 1).sum::<u64>();
let pb = if args.dry_run {
None
} else {
let pb = ProgressBar::new(operation_count);
pb.set_style(
ProgressStyle::default_bar()
.template("{msg} {bar:50} {pos}/{len}")
.unwrap(),
);
pb.set_message("Reflinking".cyan().bold().to_string());
Some(pb)
};
// TODO: to many messy branches
for group in groups {
let files = &group.files;
let src = &files[0];
for dest in files.iter().skip(1) {
pb.then(|x| x.inc(1));
let result: anyhow::Result<()> = try {
if args.use_cp_cmd.yes() {
// use `cp` command
let cmd = [
os_str!("cp"),
os_str!("--reflink"),
// archive mode
os_str!("-a"),
src.as_os_str(),
dest.as_os_str(),
];
if args.dry_run {
println!("{:?}", cmd);
} else {
let child = Command::new(cmd[0])
.args(&cmd[1..])
.stdin(Stdio::null())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let output = child.wait_with_output()?;
if !output.stderr.is_empty() {
pb.then(|x| {
x.println(format!(
"cmd stderr: {}",
String::from_utf8_lossy(&output.stderr)
));
});
}
if !output.stdout.is_empty() {
pb.then(|x| {
x.println(format!(
"cmd stdout: {}",
String::from_utf8_lossy(&output.stdout)
))
});
};
if !output.status.success() {
Err(anyhow!(
"Program exited with non-zero status: {}; cmd: {:?}",
output.status,
cmd
))?;
}
}
} else {
// use `reflink` crate
// TODO: by this approach I'm not familiar about its internal details
// and have some trouble preserving the file timestamp, which is
// important in my use case. So I by default choose using
// `ls --reflink -a` command.
if args.dry_run {
println!("{:?} -> {:?}", src, dest);
} else {
// first the dest file should be deleted
remove_file(dest)
.map_err(|e| anyhow!("Dest file lost: {}, {:?}", e, dest))?;
reflink::reflink(src, dest)?;
if !dest.exists() {
Err(anyhow!(
"Check failed: destination file doesn't exist: {:?}",
dest
))?;
}
}
}
};
if let Err(e) = result && let Some(ref b) = pb {
b.println(format!(
"Reflinking error: ({} -> {}) {}",
src.escape(),
dest.escape(),
e
));
}
}
}
Ok(())
}
trait OptionThen<T> {
fn then<F>(&self, f: F)
where
F: Fn(&T);
}
impl<T> OptionThen<T> for Option<T> {
#[inline]
fn then<F>(&self, f: F)
where
F: Fn(&T),
{
if let Some(s) = self {
f(s)
}
}
}