From 14d485a7cb86717a9d7ca653760fb666c15ff085 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Thu, 9 May 2024 22:34:16 -0400 Subject: [PATCH] feat: Copy items to the same directory closes: #135, #145 --- src/operation.rs | 126 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/src/operation.rs b/src/operation.rs index 0e3f401..9b02c9e 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,5 +1,12 @@ use cosmic::iced::futures::{channel::mpsc, executor, SinkExt}; -use std::{fs, path::PathBuf, sync::Arc}; +use std::{ + fs, + path::PathBuf, + sync::{ + atomic::{self, AtomicU64}, + Arc, + }, +}; use crate::app::Message; @@ -58,26 +65,127 @@ impl Operation { //TODO: SAFELY HANDLE CANCEL match self { Self::Copy { paths, to } => { + // Handle duplicate file names by renaming paths + let (paths, to): (Vec<_>, Vec<_>) = tokio::task::spawn_blocking(move || { + paths + .into_iter() + .zip(std::iter::repeat(to.as_path())) + .map(|(from, to)| { + log::info!("{:?}", from.parent()); + if matches!(from.parent(), Some(parent) if parent == to) { + // `from`'s parent is equal to `to` which means we're copying to the same + // directory (duplicating files) + let mut to = to.to_owned(); + let to = if let Some(full_name) = + from.file_name().and_then(|name| name.to_str()) + { + // Separate the full file name into its file name plus extension. + let (base_name, ext, needs_dot) = if full_name.starts_with('.') + { + // `[Path::file_name]` returns the full name for dotfiles (e.g. + // .someconf is the file_name) + (full_name, "", false) + } else { + // Consider everything beyond the first '.' to be a file + // extension. + full_name + .split_once('.') + .map(|(full_name, extension)| { + (full_name, extension, !extension.is_empty()) + }) + // File without an extension + .unwrap_or((full_name, "", false)) + }; + let mut n = 0u32; + // Loop until a valid `copy n` variant is found + loop { + n = if let Some(n) = n.checked_add(1) { + n + } else { + // TODO: Return error? fs_extra will handle it anyway + break to; + }; + + // Rebuild file name + let dot = if needs_dot { "." } else { "" }; + let new_name = format!("{base_name} (Copy {n}){dot}{ext}"); + to = to.join(new_name); + + if !matches!(to.try_exists(), Ok(true)) { + break to; + } + // Continue if a copy with index exists + to.pop(); + } + } else { + to + }; + + (from, to) + } else { + (from, to.to_owned()) + } + }) + .unzip() + }) + .await + .unwrap(); + let msg_tx = msg_tx.clone(); - tokio::task::spawn_blocking(move || { + tokio::task::spawn_blocking(move || -> fs_extra::error::Result<()> { log::info!("Copy {:?} to {:?}", paths, to); - let options = fs_extra::dir::CopyOptions::default(); - //TODO: set options as desired - fs_extra::copy_items_with_progress(&paths, &to, &options, |progress| { + let dir_options = fs_extra::dir::CopyOptions::default().copy_inside(true); + let file_options = fs_extra::file::CopyOptions::default(); + let copied_bytes = AtomicU64::default(); + let total_bytes = paths + .iter() + .map(fs_extra::dir::get_size) + .sum::>()?; + let handler = || { executor::block_on(async { let _ = msg_tx .lock() .await .send(Message::PendingProgress( id, - 100.0 * (progress.copied_bytes as f32) - / (progress.total_bytes as f32), + 100.0 * copied_bytes.load(atomic::Ordering::Relaxed) as f32 + / total_bytes as f32, )) .await; - }); + }) + }; + let file_handler = |progress: fs_extra::file::TransitProcess| { + copied_bytes.fetch_add(progress.copied_bytes, atomic::Ordering::Relaxed); + handler(); + }; + let dir_handler = |progress: fs_extra::TransitProcess| { + copied_bytes.fetch_add(progress.copied_bytes, atomic::Ordering::Relaxed); + handler(); //TODO: handle exceptions fs_extra::dir::TransitProcessResult::ContinueOrAbort - }) + }; + //TODO: set options as desired + for (from, to) in paths.into_iter().zip(to.into_iter()) { + // This is essentially what `[fs_extra::copy_items_with_progress]` does + // except without handling options (e.g. overwrite). We're currently using + // the defaults anyway. + if from.is_dir() { + fs_extra::copy_items_with_progress( + &[from], + to, + &dir_options, + dir_handler, + )?; + } else { + fs_extra::file::copy_with_progress( + from, + to, + &file_options, + file_handler, + )?; + } + } + Ok(()) }) .await .map_err(err_str)?