use crate::config::generate::{ExecMode, InteractiveMode};
use crate::library::file_ops::is_same_file_contents;
use crate::library::results::{HttmError, HttmResult};
use crate::zfs::run_command::RunZFSCommand;
use crate::{GLOBAL_CONFIG, IN_BUFFER_SIZE};
use indicatif::{ProgressBar, ProgressStyle};
use std::borrow::Cow;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, ErrorKind, Seek, SeekFrom, Write};
use std::os::fd::{AsFd, BorrowedFd};
use std::path::Path;
use std::sync::LazyLock;
use std::sync::atomic::AtomicBool;
static IS_CLONE_COMPATIBLE: LazyLock<AtomicBool> = LazyLock::new(|| {
let Ok(zfs_cmd) = RunZFSCommand::new() else {
return AtomicBool::new(false);
};
match zfs_cmd.version() {
Err(_) => return AtomicBool::new(false),
Ok(stdout)
if stdout.contains("zfs-2.2.0")
|| stdout.contains("zfs-kmod-2.2.0")
|| stdout.contains("zfs-2.2.1")
|| stdout.contains("zfs-kmod-2.2.1")
|| stdout.contains("zfs-2.2-")
|| stdout.contains("zfs-kmod-2.2-") =>
{
return AtomicBool::new(false);
}
Ok(_) => return AtomicBool::new(true),
}
});
enum DstFileState {
Exists,
DoesNotExist,
}
impl DstFileState {
fn exists(dst_file: &File) -> Self {
if dst_file.metadata().is_ok() {
DstFileState::Exists
} else {
DstFileState::DoesNotExist
}
}
}
pub struct HttmCopy;
impl HttmCopy {
pub fn new(src: &Path, dst: &Path) -> HttmResult<()> {
let src_file = std::fs::OpenOptions::new().read(true).open(src)?;
let src_len = src.symlink_metadata()?.len();
let mut dst_file = OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(dst)?;
dst_file.set_len(src_len)?;
let file_name = src.file_name().unwrap_or_default().to_string_lossy();
let opt_bar = Self::opt_bar(file_name, src_len)?;
if !GLOBAL_CONFIG.opt_no_clones
&& IS_CLONE_COMPATIBLE.load(std::sync::atomic::Ordering::Relaxed)
{
match CloneCopy::new(&src_file, &mut dst_file, opt_bar.as_ref()) {
Ok(_) => {
if GLOBAL_CONFIG.opt_debug {
eprintln!("DEBUG: copy_file_range call successful.");
}
return Ok(());
}
Err(err) => {
IS_CLONE_COMPATIBLE.store(false, std::sync::atomic::Ordering::Relaxed);
if GLOBAL_CONFIG.opt_debug {
if GLOBAL_CONFIG.opt_debug {
eprintln!(
"DEBUG: copy_file_range call unsuccessful for the following reason: \"{:?}\".\n
DEBUG: Retrying a conventional diff copy.",
err
);
}
}
}
}
}
DiffCopy::new(&src_file, &mut dst_file, opt_bar.as_ref())?;
if GLOBAL_CONFIG.opt_debug {
eprintln!("DEBUG: Write to file completed. Confirmation initiated.");
Self::confirm(src, dst)?;
}
Ok(())
}
fn opt_bar(file_name: Cow<str>, len: u64) -> HttmResult<Option<ProgressBar>> {
match GLOBAL_CONFIG.exec_mode {
ExecMode::Interactive(InteractiveMode::Restore(_)) => {
let bar = ProgressBar::new(len).with_style(ProgressStyle::with_template(
"[{decimal_total_bytes}] {bar:40.cyan/blue} {msg}",
)?);
bar.set_message(file_name.to_string());
Ok(Some(bar))
}
_ if len.gt(&1_000_000_000) => {
let bar = ProgressBar::new(len).with_style(ProgressStyle::with_template(
"[{decimal_total_bytes}] {bar:40.cyan/blue} {msg}",
)?);
bar.set_message(file_name.to_string());
Ok(Some(bar))
}
_ => Ok(None),
}
}
pub fn confirm(src: &Path, dst: &Path) -> HttmResult<()> {
if is_same_file_contents(src, dst) {
Ok(())
} else {
let description = format!(
"Copy failed. File contents of {} and {} are NOT the same.",
src.display(),
dst.display()
);
HttmError::from(description).into()
}
}
}
pub struct CloneCopy;
impl CloneCopy {
fn new(src_file: &File, dst_file: &mut File, opt_bar: Option<&ProgressBar>) -> HttmResult<()> {
let src_len = src_file.metadata()?.len();
let src_fd = src_file.as_fd();
let dst_fd = dst_file.as_fd();
if let Err(err) = Self::copy_file_range(src_fd, dst_fd, src_len, opt_bar) {
let description =
format!("DEBUG: copy_file_range call unsuccessful for the following reason");
return HttmError::with_source(description, err.as_ref()).into();
}
dst_file.flush()?;
dst_file.sync_data()?;
Ok(())
}
#[allow(unreachable_code, unused_variables)]
#[inline]
fn copy_file_range(
src_file_fd: BorrowedFd,
dst_file_fd: BorrowedFd,
len: u64,
opt_bar: Option<&ProgressBar>,
) -> HttmResult<()> {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
let mut amt_written = 0u64;
let mut remainder = len as usize;
while remainder > 0 {
let mut off_src = amt_written as i64;
let mut off_dst = off_src.clone();
match nix::fcntl::copy_file_range(
src_file_fd,
Some(&mut off_src),
dst_file_fd,
Some(&mut off_dst),
remainder,
) {
Ok(bytes_written) if bytes_written == 0 && remainder != 0 => break,
Ok(bytes_written) => {
amt_written += bytes_written as u64;
remainder = len.saturating_sub(amt_written) as usize;
if let Some(ref bar) = opt_bar {
bar.inc(bytes_written as u64)
}
if amt_written > len {
return Err(
HttmError::new("Amount written larger than file len.").into()
);
}
}
Err(err) => match err {
nix::errno::Errno::ENOSYS => {
return HttmError::new(
"Operating system does not support copy_file_ranges.",
)
.into();
}
_ => {
if GLOBAL_CONFIG.opt_debug {
eprintln!(
"DEBUG: copy_file_range call failed for the following reason: {}\nDEBUG: Falling back to default diff copy behavior.",
err
);
}
return Err(Box::new(err));
}
},
}
}
if let Some(ref bar) = opt_bar {
bar.finish_and_clear()
}
return Ok(());
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
HttmError::new("Operating system does not support copy_file_ranges.").into()
}
}
pub struct DiffCopy;
impl DiffCopy {
fn new(src_file: &File, dst_file: &mut File, opt_bar: Option<&ProgressBar>) -> HttmResult<()> {
Self::write_no_cow(&src_file, &dst_file, opt_bar)?;
dst_file.flush()?;
dst_file.sync_data()?;
Ok(())
}
#[inline]
fn write_no_cow(
src_file: &File,
dst_file: &File,
opt_bar: Option<&ProgressBar>,
) -> HttmResult<()> {
let mut src_reader = BufReader::with_capacity(IN_BUFFER_SIZE, src_file);
let mut dst_reader = BufReader::with_capacity(IN_BUFFER_SIZE, dst_file);
let mut dst_writer = BufWriter::with_capacity(IN_BUFFER_SIZE, dst_file);
let dst_exists = DstFileState::exists(dst_file);
let mut cur_pos = 0u64;
loop {
match src_reader.fill_buf() {
Ok(src_read) => {
let src_amt_read = src_read.len();
if src_amt_read == 0 {
break;
}
match dst_exists {
DstFileState::DoesNotExist => {
Self::write_to_offset(&mut dst_writer, src_read, cur_pos)?;
}
DstFileState::Exists => {
match dst_reader.fill_buf() {
Ok(dst_read) => {
if !Self::is_same_bytes(src_read, dst_read) {
Self::write_to_offset(&mut dst_writer, src_read, cur_pos)?
}
let dst_amt_read = dst_read.len();
dst_reader.consume(dst_amt_read);
}
Err(err) => match err.kind() {
ErrorKind::Interrupted => continue,
ErrorKind::UnexpectedEof => {
break;
}
_ => return Err(err.into()),
},
}
}
};
if let Some(ref bar) = opt_bar {
bar.inc(src_amt_read as u64)
}
cur_pos += src_amt_read as u64;
src_reader.consume(src_amt_read);
}
Err(err) => match err.kind() {
ErrorKind::Interrupted => continue,
ErrorKind::UnexpectedEof => {
break;
}
_ => return Err(err.into()),
},
};
}
if let Some(ref bar) = opt_bar {
bar.finish_and_clear();
}
Ok(())
}
#[inline]
fn is_same_bytes(a_bytes: &[u8], b_bytes: &[u8]) -> bool {
let (a_hash, b_hash): (u64, u64) =
rayon::join(|| Self::hash(a_bytes), || Self::hash(b_bytes));
a_hash == b_hash
}
#[inline]
fn hash(bytes: &[u8]) -> u64 {
use foldhash::quality::FixedState;
use std::hash::{BuildHasher, Hasher};
let s = FixedState::default();
let mut hash = s.build_hasher();
hash.write(bytes);
hash.finish()
}
#[inline]
fn write_to_offset(
dst_writer: &mut BufWriter<&File>,
src_read: &[u8],
cur_pos: u64,
) -> HttmResult<()> {
dst_writer.seek(SeekFrom::Start(cur_pos))?;
dst_writer.write_all(src_read)?;
Ok(())
}
}