cosmic-files/src/operation/recursive.rs
2024-11-14 14:43:45 -07:00

309 lines
10 KiB
Rust

use std::{
error::Error,
fs,
io::{Read, Write},
ops::ControlFlow,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use walkdir::WalkDir;
use super::{copy_unique_path, ReplaceResult};
use crate::fl;
pub struct Context {
buf: Vec<u8>,
cancelled: Arc<AtomicBool>,
on_progress: Box<dyn Fn(&Op, &Progress) + 'static>,
on_replace: Box<dyn Fn(&Op) -> ReplaceResult + 'static>,
replace_result_opt: Option<ReplaceResult>,
}
impl Context {
pub fn new(cancelled: Arc<AtomicBool>) -> Self {
Self {
buf: vec![0; 4 * 1024 * 1024],
cancelled,
on_progress: Box::new(|_op, _progress| {}),
on_replace: Box::new(|_op| ReplaceResult::Cancel),
replace_result_opt: None,
}
}
pub fn recursive_copy_or_move(
&mut self,
from_to_pairs: Vec<(PathBuf, PathBuf)>,
moving: bool,
) -> Result<bool, String> {
let mut ops = Vec::new();
let mut cleanup_ops = Vec::new();
for (from_parent, to_parent) in from_to_pairs {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
if from_parent == to_parent {
// Skip matching source and destination
continue;
}
for entry in WalkDir::new(&from_parent).into_iter() {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
let entry = entry.map_err(|err| {
format!("failed to walk directory {:?}: {}", from_parent, err)
})?;
let file_type = entry.file_type();
let from = entry.into_path();
let kind = if file_type.is_dir() {
OpKind::Mkdir
} else if file_type.is_file() {
if moving {
OpKind::Move
} else {
OpKind::Copy
}
} else if file_type.is_symlink() {
let target = fs::read_link(&from)
.map_err(|err| format!("failed to read link {:?}: {}", from, err))?;
OpKind::Symlink { target }
} else {
//TODO: present dialog and allow continue
return Err(format!("{} is not a known file type", from.display()).into());
};
let to = if from == from_parent {
// When copying a file, from matches from_parent, and to_parent must be used
to_parent.clone()
} else {
let relative = from.strip_prefix(&from_parent).map_err(|err| {
format!(
"failed to remove prefix {:?} from {:?}: {}",
from_parent, from, err
)
})?;
//TODO: ensure to is inside of to_parent?
to_parent.join(relative)
};
let op = Op { kind, from, to };
if moving {
if let Some(cleanup_op) = op.move_cleanup_op() {
cleanup_ops.push(cleanup_op);
}
}
ops.push(op);
}
}
// Add cleanup ops after standard ops, in reverse
for cleanup_op in cleanup_ops.into_iter().rev() {
ops.push(cleanup_op);
}
let total_ops = ops.len();
for (current_ops, mut op) in ops.into_iter().enumerate() {
if self.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled"));
}
let progress = Progress {
current_ops,
total_ops,
current_bytes: 0,
total_bytes: None,
};
(self.on_progress)(&op, &progress);
if !op.run(self, progress).map_err(|err| {
format!(
"failed to {:?} {:?} to {:?}: {}",
op.kind, op.from, op.to, err
)
})? {
return Ok(false);
}
}
Ok(true)
}
pub fn on_progress<F: Fn(&Op, &Progress) + 'static>(mut self, f: F) -> Self {
self.on_progress = Box::new(f);
self
}
pub fn on_replace<F: Fn(&Op) -> ReplaceResult + 'static>(mut self, f: F) -> Self {
self.on_replace = Box::new(f);
self
}
fn replace(&mut self, op: &Op) -> Result<ControlFlow<bool, PathBuf>, Box<dyn Error>> {
let replace_result = self
.replace_result_opt
.unwrap_or_else(|| (self.on_replace)(op));
match replace_result {
ReplaceResult::Replace(apply_to_all) => {
if apply_to_all {
self.replace_result_opt = Some(replace_result);
}
fs::remove_file(&op.to)?;
Ok(ControlFlow::Continue(op.to.clone()))
}
ReplaceResult::KeepBoth => match op.to.parent() {
Some(to_parent) => Ok(ControlFlow::Continue(copy_unique_path(
&op.from, &to_parent,
))),
None => Err(format!("failed to get parent of {:?}", op.to).into()),
},
ReplaceResult::Skip(apply_to_all) => {
if apply_to_all {
self.replace_result_opt = Some(replace_result);
}
Ok(ControlFlow::Break(true))
}
ReplaceResult::Cancel => Ok(ControlFlow::Break(false)),
}
}
}
#[derive(Debug)]
pub struct Progress {
pub current_ops: usize,
pub total_ops: usize,
pub current_bytes: u64,
pub total_bytes: Option<u64>,
}
#[derive(Debug)]
pub enum OpKind {
Copy,
Move,
Mkdir,
Remove,
Rmdir,
Symlink { target: PathBuf },
}
#[derive(Debug)]
pub struct Op {
pub kind: OpKind,
pub from: PathBuf,
pub to: PathBuf,
}
impl Op {
fn move_cleanup_op(&self) -> Option<Self> {
let kind = match self.kind {
OpKind::Copy | OpKind::Move | OpKind::Symlink { .. } => OpKind::Remove,
OpKind::Mkdir => OpKind::Rmdir,
OpKind::Remove | OpKind::Rmdir => return None,
};
Some(Self {
kind,
from: self.from.clone(),
//TODO: it is strange to have `to` here
to: self.to.clone(),
})
}
fn run(&mut self, ctx: &mut Context, mut progress: Progress) -> Result<bool, Box<dyn Error>> {
match self.kind {
OpKind::Copy => {
let mut from_file = fs::OpenOptions::new().read(true).open(&self.from)?;
let metadata = from_file.metadata()?;
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
}
progress.total_bytes = Some(metadata.len());
(ctx.on_progress)(&self, &progress);
// This is atomic and ensures `to` is not created by any other process
let mut to_file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&self.to)?;
to_file.set_permissions(metadata.permissions())?;
loop {
if ctx.cancelled.load(Ordering::SeqCst) {
return Err(fl!("cancelled").into());
}
let count = from_file.read(&mut ctx.buf)?;
if count == 0 {
break;
}
to_file.write_all(&ctx.buf[..count])?;
progress.current_bytes += count as u64;
(ctx.on_progress)(&self, &progress);
}
to_file.sync_all()?;
}
OpKind::Move => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
}
// This is atomic and ensures `to` is not created by any other process
match fs::hard_link(&self.from, &self.to) {
Ok(()) => {}
Err(err) => {
//TODO: what is the error code on Windows?
if err.raw_os_error() == Some(libc::EXDEV) {
// Try standard copy if hard link fails with cross device error
let mut copy_op = Op {
kind: OpKind::Copy,
from: self.from.clone(),
to: self.to.clone(),
};
copy_op.run(ctx, progress)?;
} else {
return Err(err.into());
}
}
}
}
OpKind::Mkdir => {
fs::create_dir_all(&self.to)?;
}
OpKind::Remove => {
fs::remove_file(&self.from)?;
}
OpKind::Rmdir => {
fs::remove_dir(&self.from)?;
}
OpKind::Symlink { ref target } => {
// Remove `to` if overwriting and it is an existing file
if self.to.is_file() {
match ctx.replace(&self)? {
ControlFlow::Continue(to) => {
self.to = to;
}
ControlFlow::Break(ret) => {
return Ok(ret);
}
}
}
//TODO: use OS-specific function
fs::soft_link(&target, &self.to)?;
}
}
Ok(true)
}
}