use super::*;
pub enum Step<'a> {
Starting(&'a Path),
Advanced(u64),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Transfer {
Done,
Cancelled,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Outcome {
Done,
Skipped,
Cancelled,
}
impl From<Outcome> for Transfer {
fn from(outcome: Outcome) -> Self {
match outcome {
Outcome::Cancelled => Transfer::Cancelled,
Outcome::Done | Outcome::Skipped => Transfer::Done,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Reply {
Retry,
Skip,
Abort,
Delete,
}
pub trait Progress {
fn step(&mut self, step: Step<'_>) -> bool;
fn failed(&mut self, _path: &Path, _error: &Error) -> Reply {
Reply::Abort
}
fn exists(&mut self, _source: &Path, _dest: &Path) -> Clash {
Clash::Replace
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Clash {
Replace,
Keep,
Abort,
}
impl<F: FnMut(Step<'_>) -> bool> Progress for F {
fn step(&mut self, step: Step<'_>) -> bool {
self(step)
}
}
pub type Report<'a> = &'a mut dyn Progress;
pub(super) fn attempt<T>(path: &Path, report: Report<'_>, mut op: impl FnMut(Report<'_>) -> Result<T, Error>) -> Result<Option<T>, Error> {
loop {
match op(&mut *report) {
Ok(value) => return Ok(Some(value)),
Err(error) => match report.failed(path, &error) {
Reply::Retry => {}
Reply::Skip | Reply::Delete => return Ok(None),
Reply::Abort => return Err(error),
},
}
}
}
pub(super) fn next_entry(dir: &Path, entry: Result<fs::DirEntry, Error>, report: Report<'_>) -> Result<Option<(fs::DirEntry, fs::FileType)>, Error> {
match entry.and_then(|entry| entry.file_type().map(|file_type| (entry, file_type))) {
Ok(pair) => Ok(Some(pair)),
Err(error) => match report.failed(dir, &error) {
Reply::Abort => Err(error),
Reply::Retry | Reply::Skip | Reply::Delete => Ok(None),
},
}
}
pub fn rename_in_place(source: &Path, dest: &Path) -> bool {
rename(source, dest).is_ok()
}
pub fn measure(items: &[(PathBuf, PathBuf, bool)]) -> u64 {
items
.iter()
.map(|(source, _, is_dir)| {
if fs::symlink_metadata(source).map(|meta| meta.file_type().is_symlink()).unwrap_or(false) {
0
} else if *is_dir {
calculate_dir_size(source).unwrap_or(0)
} else {
fs::metadata(source).map(|meta| meta.len()).unwrap_or(0)
}
})
.sum()
}
pub fn copies_into_itself(source: &Path, dest: &Path) -> bool {
if fs::symlink_metadata(source).map(|meta| meta.file_type().is_symlink()).unwrap_or(false) || !source.is_dir() {
return false;
}
let resolve = |path: &Path| path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let source = resolve(source);
let dest = match (dest.parent(), dest.file_name()) {
(Some(parent), Some(name)) => resolve(parent).join(name),
_ => resolve(dest),
};
dest.starts_with(&source)
}
pub fn copy_path(source: PathBuf, dest: PathBuf, is_dir: bool, report: Report<'_>) -> Result<Transfer, Error> {
if copies_into_itself(&source, &dest) {
return Err(Error::new(ErrorKind::InvalidInput, format!("Cannot copy \"{}\" into itself", source.display())));
}
let Some(metadata) = attempt(&source, report, |_| fs::symlink_metadata(&source))? else {
return Ok(Transfer::Done);
};
let outcome = if metadata.file_type().is_symlink() {
settle_clash(&source, &dest, report).map_or_else(|| copy_link(&source, &dest, report), Ok)?
} else if is_dir {
copy_dir_recursive(&source, &dest, report)?
} else {
settle_clash(&source, &dest, report).map_or_else(|| copy_file(&source, &dest, report), Ok)?
};
Ok(outcome.into())
}
pub(super) fn settle_clash(source: &Path, dest: &Path, report: Report<'_>) -> Option<Outcome> {
let existing = dest.symlink_metadata().ok()?;
if existing.is_dir() {
return None;
}
match report.exists(source, dest) {
Clash::Replace => None,
Clash::Keep => {
let bytes = source.symlink_metadata().map_or(0, |metadata| if metadata.is_file() { metadata.len() } else { 0 });
let carry_on = report.step(Step::Starting(source)) && report.step(Step::Advanced(bytes));
Some(if carry_on { Outcome::Skipped } else { Outcome::Cancelled })
}
Clash::Abort => Some(Outcome::Cancelled),
}
}
fn copy_link(source: &Path, dest: &Path, report: Report<'_>) -> Result<Outcome, Error> {
let copied = attempt(source, report, |_| {
clear_for_link(dest)?;
copy_symlink(source, dest)
})?;
Ok(if copied.is_some() { Outcome::Done } else { Outcome::Skipped })
}
fn copy_file(source: &Path, dest: &Path, report: Report<'_>) -> Result<Outcome, Error> {
Ok(match attempt(source, report, |report| copy_file_content(source, dest, report))? {
Some(Transfer::Done) => Outcome::Done,
Some(Transfer::Cancelled) => Outcome::Cancelled,
None => Outcome::Skipped,
})
}
#[cfg(unix)]
fn copy_symlink(source: &Path, dest: &Path) -> Result<(), Error> {
std::os::unix::fs::symlink(fs::read_link(source)?, dest)
}
#[cfg(windows)]
fn copy_symlink(source: &Path, dest: &Path) -> Result<(), Error> {
let target = fs::read_link(source)?;
if source.is_dir() { std::os::windows::fs::symlink_dir(target, dest) } else { std::os::windows::fs::symlink_file(target, dest) }
}
fn copy_file_content(source: &Path, dest: &Path, report: Report<'_>) -> Result<Transfer, Error> {
let metadata = fs::symlink_metadata(source)?;
if !metadata.is_file() {
return Err(not_regular(source));
}
if !report.step(Step::Starting(source)) {
return Ok(Transfer::Cancelled);
}
let mut src_file = File::open(source)?;
write_beside(dest, &mut src_file, report, &|dst_file, temp| {
keep_times(dst_file, &metadata);
let _ = fs::set_permissions(temp, metadata.permissions());
})
}
pub(super) fn write_beside<R: Read + ?Sized>(dest: &Path, reader: &mut R, report: Report<'_>, finish: &dyn Fn(&File, &Path)) -> Result<Transfer, Error> {
let replacing = dest.symlink_metadata().is_ok();
let (temp, mut dst_file) = temp_beside(dest)?;
let discard = |dst_file: File| {
drop(dst_file);
let _ = remove_file(&temp);
};
loop {
let copied = match io::copy(&mut (&mut *reader).take(COPY_CHUNK), &mut dst_file) {
Ok(copied) => copied,
Err(e) => {
discard(dst_file);
return Err(e);
}
};
if copied == 0 {
finish(&dst_file, &temp);
drop(dst_file);
place(&temp, dest, replacing)?;
return Ok(Transfer::Done);
}
if !report.step(Step::Advanced(copied)) {
discard(dst_file);
return Ok(Transfer::Cancelled);
}
}
}
pub(super) fn place(temp: &Path, dest: &Path, replacing: bool) -> Result<(), Error> {
let placed = if replacing {
rename(temp, dest).or_else(|_| clear_link(dest).and_then(|()| rename(temp, dest)))
} else if path_exists(dest) {
Err(Error::new(ErrorKind::AlreadyExists, format!("Destination already exists: {}", dest.display())))
} else {
rename(temp, dest)
};
if placed.is_err() {
let _ = remove_file(temp);
}
placed
}
pub(super) fn keep_times(dest: &File, metadata: &fs::Metadata) {
let mut times = fs::FileTimes::new();
if let Ok(modified) = metadata.modified() {
times = times.set_modified(modified);
}
if let Ok(accessed) = metadata.accessed() {
times = times.set_accessed(accessed);
}
let _ = dest.set_times(times);
}
pub(super) fn keep_dir_times(dest: &Path, metadata: &fs::Metadata) {
if let Ok(dir) = File::open(dest) {
keep_times(&dir, metadata);
}
}
fn copy_dir_recursive(source: &Path, dest: &Path, report: Report<'_>) -> Result<Outcome, Error> {
if attempt(dest, report, |_| fs::create_dir_all(dest))?.is_none() {
return Ok(Outcome::Skipped);
}
let Some(entries) = attempt(source, report, |_| read_dir(source))? else {
return Ok(Outcome::Skipped);
};
let mut skipped = false;
for entry in entries {
let Some((entry, file_type)) = next_entry(source, entry, report)? else {
skipped = true;
continue;
};
let entry_path = entry.path();
let dest_path = dest.join(entry.file_name());
let outcome = if file_type.is_dir() {
copy_dir_recursive(&entry_path, &dest_path, &mut *report)?
} else if let Some(outcome) = settle_clash(&entry_path, &dest_path, &mut *report) {
outcome
} else if file_type.is_symlink() {
copy_link(&entry_path, &dest_path, &mut *report)?
} else {
copy_file(&entry_path, &dest_path, &mut *report)?
};
match outcome {
Outcome::Cancelled => return Ok(Outcome::Cancelled),
Outcome::Skipped => skipped = true,
Outcome::Done => {}
}
}
if let Ok(metadata) = fs::metadata(source) {
keep_dir_times(dest, &metadata);
let _ = fs::set_permissions(dest, metadata.permissions());
}
Ok(if skipped { Outcome::Skipped } else { Outcome::Done })
}
pub fn move_path(source: PathBuf, dest: PathBuf, report: Report<'_>) -> Result<Transfer, Error> {
if path_exists(&dest) {
return Ok(move_across(&source, &dest, report)?.into());
}
let renamed = attempt(&source, report, |_| match rename(&source, &dest) {
Ok(()) => Ok(true),
Err(e) if is_cross_device(&e) => Ok(false),
Err(e) => Err(e),
})?;
match renamed {
Some(true) | None => Ok(Transfer::Done),
Some(false) => Ok(move_across(&source, &dest, report)?.into()),
}
}
fn move_across(source: &Path, dest: &Path, report: Report<'_>) -> Result<Outcome, Error> {
let Some(metadata) = attempt(source, report, |_| fs::symlink_metadata(source))? else {
return Ok(Outcome::Skipped);
};
if !metadata.is_dir() && is_same_entry(source, dest) {
let bytes = if metadata.is_file() { metadata.len() } else { 0 };
if !report.step(Step::Starting(source)) || !report.step(Step::Advanced(bytes)) {
return Ok(Outcome::Cancelled);
}
let removed = attempt(source, report, |_| remove_file(source))?;
return Ok(if removed.is_some() { Outcome::Done } else { Outcome::Skipped });
}
if !metadata.is_dir()
&& let Some(outcome) = settle_clash(source, dest, report)
{
return Ok(outcome);
}
if rename(source, dest).is_ok() {
if !metadata.is_dir() {
let bytes = if metadata.file_type().is_symlink() { 0 } else { metadata.len() };
if !report.step(Step::Starting(source)) || !report.step(Step::Advanced(bytes)) {
return Ok(Outcome::Cancelled);
}
}
return Ok(Outcome::Done);
}
if !metadata.is_dir() {
let copied = if metadata.file_type().is_symlink() { copy_link(source, dest, report)? } else { copy_file(source, dest, report)? };
if copied != Outcome::Done {
return Ok(copied);
}
let removed = attempt(source, report, |_| remove_file(source))?;
return Ok(if removed.is_some() { Outcome::Done } else { Outcome::Skipped });
}
if attempt(dest, report, |_| fs::create_dir_all(dest))?.is_none() {
return Ok(Outcome::Skipped);
}
let Some(entries) = attempt(source, report, |_| read_dir(source))? else {
return Ok(Outcome::Skipped);
};
let mut skipped = false;
for entry in entries {
let Some((entry, _)) = next_entry(source, entry, report)? else {
skipped = true;
continue;
};
match move_across(&entry.path(), &dest.join(entry.file_name()), &mut *report)? {
Outcome::Cancelled => return Ok(Outcome::Cancelled),
Outcome::Skipped => skipped = true,
Outcome::Done => {}
}
}
if skipped {
return Ok(Outcome::Skipped);
}
keep_dir_times(dest, &metadata);
let _ = fs::set_permissions(dest, metadata.permissions());
let removed = attempt(source, report, |_| remove_dir(source))?;
Ok(if removed.is_some() { Outcome::Done } else { Outcome::Skipped })
}
fn is_real_dir(path: &Path) -> bool {
path.symlink_metadata().is_ok_and(|metadata| metadata.is_dir())
}
pub fn transfer_targets(sources: &[(PathBuf, bool)], destination: &Path, names_dir: bool) -> Result<Vec<(PathBuf, PathBuf, bool)>, String> {
if destination.is_dir() {
return Ok(sources.iter().map(|(source, is_dir)| (source.clone(), destination.join(source.file_name().unwrap_or_default()), *is_dir)).collect());
}
if names_dir {
return Err(format!("No such directory: {}", destination.display()));
}
let [(source, is_dir)] = sources else {
return Err(format!("{} items need a directory to go into, and {} is not one", sources.len(), destination.display()));
};
match destination.parent() {
Some(parent) if parent.is_dir() => Ok(vec![(source.clone(), destination.to_path_buf(), *is_dir)]),
Some(parent) => Err(format!("No such directory: {}", parent.display())),
None => Err(format!("Cannot copy to {}", destination.display())),
}
}
pub fn check_destinations(items: &[(PathBuf, PathBuf, bool)]) -> Result<Vec<PathBuf>, String> {
let mut taken = Vec::new();
for (source, dest, _) in items {
if !path_exists(dest) {
continue;
}
if is_same_entry(source, dest) {
return Err(format!("\"{}\" is already there", source.display()));
}
match (is_real_dir(source), is_real_dir(dest)) {
(false, true) => return Err(format!("Cannot replace directory {} with a file", dest.display())),
(true, false) => return Err(format!("Cannot replace file {} with a directory", dest.display())),
_ => {}
}
taken.push(dest.clone());
}
if let Some((source, _, _)) = items.iter().find(|(source, dest, _)| copies_into_itself(source, dest)) {
return Err(format!("Cannot copy \"{}\" into itself", source.display()));
}
Ok(taken)
}
pub(super) fn is_cross_device(error: &Error) -> bool {
error.kind() == ErrorKind::CrossesDevices
}
#[cfg(test)]
mod tests;