use anyhow::{anyhow, Result};
use filetime::FileTime;
use log::{debug, error, trace};
use std::fmt::{Display, Formatter};
use std::fs;
use std::path::Path;
use std::str::FromStr;
use std::sync::MutexGuard;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ActualAction {
Move,
Copy,
Hardlink,
RelativeSymlink,
AbsoluteSymlink,
}
impl Display for ActualAction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ActualAction::Move => write!(f, "Move"),
ActualAction::Copy => write!(f, "Copy"),
ActualAction::Hardlink => write!(f, "Hardlink"),
ActualAction::RelativeSymlink => write!(f, "RelSymlink"),
ActualAction::AbsoluteSymlink => write!(f, "AbsSymlink"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ActionMode {
Execute(ActualAction),
DryRun(ActualAction),
}
impl FromStr for ActualAction {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
match s.to_lowercase().as_str() {
"move" => Ok(ActualAction::Move),
"copy" => Ok(ActualAction::Copy),
"hardlink" | "hard" => Ok(ActualAction::Hardlink),
"relative_symlink" | "relsym" => Ok(ActualAction::RelativeSymlink),
"absolute_symlink" | "abssym" => Ok(ActualAction::AbsoluteSymlink),
_ => Err(anyhow::anyhow!("Invalid action mode")),
}
}
}
pub fn file_action<P: AsRef<Path>, Q: AsRef<Path>>(
source: P,
target: Q,
action: &ActionMode,
mkdir: bool,
guard: Option<MutexGuard<()>>,
) -> Result<()> {
let source = source.as_ref();
let target = target.as_ref();
error_file_exists(target)
.map_err(|e| anyhow!("Target file already exists: {target:?} - {e:?}"))?;
if let Some(parent) = target.parent() {
if !parent.exists() {
if !mkdir {
return Err(anyhow!(
"Target subfolder does not exist. Use --mkdir to create it: {parent:?}"
));
}
if matches!(action, ActionMode::DryRun(_)) {
error!("[Mkdir] {}", parent.display());
} else {
fs::create_dir_all(parent).map_err(|e| {
anyhow!("Failed to create target subfolder: {parent:?} - {e:?}")
})?;
}
}
}
let result = match action {
ActionMode::Execute(ActualAction::Move) => move_file(source, target, guard),
ActionMode::Execute(ActualAction::Copy) => copy_file(source, target, guard),
ActionMode::Execute(ActualAction::Hardlink) => hardlink_file(source, target, guard),
ActionMode::Execute(ActualAction::RelativeSymlink) => relative_symlink_file(source, target),
ActionMode::Execute(ActualAction::AbsoluteSymlink) => absolute_symlink_file(source, target),
ActionMode::DryRun(action) => {
dry_run(source, target, *action);
Ok(())
}
};
match result {
Ok(()) => Ok(()),
Err(e) => Err(anyhow!("Failed to perform action: {e:?}")),
}
}
fn dry_run<A: AsRef<Path>, B: AsRef<Path>>(source: A, target: B, action: ActualAction) {
error!(
"[{}] {} -> {}",
action,
source.as_ref().display(),
target.as_ref().display()
);
}
fn error_file_exists(target: &Path) -> std::io::Result<()> {
if target.exists() {
Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"Target file already exists",
))
} else {
Ok(())
}
}
fn copy_file<A: AsRef<Path>, B: AsRef<Path>>(
source: A,
target: B,
guard: Option<MutexGuard<()>>,
) -> std::io::Result<()> {
let source = source.as_ref();
let target = target.as_ref();
debug!("Copying {} -> {}", source.display(), target.display());
fs::write(target, [])?; drop(guard);
let metadata = fs::metadata(source)?;
let result = fs::copy(source, target)?;
if metadata.len() != result {
let _ = fs::remove_file(target);
return Err(std::io::Error::other("File copy failed"));
}
let mtime = FileTime::from_last_modification_time(&metadata);
let atime = FileTime::from_last_access_time(&metadata);
filetime::set_file_times(target, atime, mtime)?;
Ok(())
}
fn move_file<A: AsRef<Path>, B: AsRef<Path>>(
source: A,
target: B,
guard: Option<MutexGuard<()>>,
) -> std::io::Result<()> {
let source = source.as_ref();
let target = target.as_ref();
debug!("Moving {} -> {}", source.display(), target.display());
let result = fs::rename(source, target);
if let Err(err) = result {
trace!(
"Renaming file failed, falling back to cut/paste (maybe cross-fs rename operation?): {:?} for file {} -> {}",
err,
source.display(),
target.display()
);
copy_file(source, target, guard)?;
fs::remove_file(source)
} else {
Ok(())
}
}
fn hardlink_file<A: AsRef<Path>, B: AsRef<Path>>(
source: A,
target: B,
guard: Option<MutexGuard<()>>,
) -> std::io::Result<()> {
let source = source.as_ref();
let target = target.as_ref();
debug!(
"Creating hardlink {} -> {}",
source.display(),
target.display()
);
let result = fs::hard_link(source, target);
if let Err(err) = result {
error!(
"Creating hardlink failed, falling back to copy: {:?} for file {} -> {}",
err,
source.display(),
target.display()
);
copy_file(source, target, guard)
} else {
Ok(())
}
}
fn relative_symlink_file<A: AsRef<Path>, B: AsRef<Path>>(
source: A,
target: B,
) -> std::io::Result<()> {
let source = source.as_ref();
let target = target.as_ref();
debug!(
"Creating symlink {} -> {}",
source.display(),
target.display()
);
symlink::symlink_file(source, target)?;
Ok(())
}
fn absolute_symlink_file<A: AsRef<Path>, B: AsRef<Path>>(
source: A,
target: B,
) -> std::io::Result<()> {
let source = source.as_ref();
let target = target.as_ref();
debug!(
"Creating symlink {} -> {}",
source.display(),
target.display()
);
let source = fs::canonicalize(source)?;
relative_symlink_file(&source, target)
}