diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index b18f5d9..a2e590a 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -26,6 +26,11 @@ modified = Modified trashed-on = Trashed size = Size +# Progress footer +details = Details +dismiss = Dismiss message +operations-in-progress = {$count} actions in progress ({$percent}%)... + # Dialogs ## Compress Dialog @@ -126,6 +131,7 @@ try-again = Try again username = Username ## Operations +cancelled = Cancelled edit-history = Edit history history = History no-history = No items in history. @@ -135,7 +141,7 @@ complete = Complete compressing = Compressing {$items} {$items -> [one] item *[other] items - } from "{$from}" to "{$to}" + } from "{$from}" to "{$to}" ({$percent}%)... compressed = Compressed {$items} {$items -> [one] item *[other] items @@ -146,17 +152,17 @@ created = Created "{$name}" in "{$parent}" copying = Copying {$items} {$items -> [one] item *[other] items - } from "{$from}" to "{$to}" + } from "{$from}" to "{$to}" ({$percent}%)... copied = Copied {$items} {$items -> [one] item *[other] items } from "{$from}" to "{$to}" -emptying-trash = Emptying {trash} +emptying-trash = Emptying {trash} ({$percent}%)... emptied-trash = Emptied {trash} extracting = Extracting {$items} {$items -> [one] item *[other] items - } from "{$from}" to "{$to}" + } from "{$from}" to "{$to}" ({$percent}%)... extracted = Extracted {$items} {$items -> [one] item *[other] items @@ -166,7 +172,7 @@ set-executable-and-launched = Set "{$name}" as executable and launched moving = Moving {$items} {$items -> [one] item *[other] items - } from "{$from}" to "{$to}" + } from "{$from}" to "{$to}" ({$percent}%)... moved = Moved {$items} {$items -> [one] item *[other] items @@ -176,7 +182,7 @@ renamed = Renamed "{$from}" to "{$to}" restoring = Restoring {$items} {$items -> [one] item *[other] items - } from {trash} + } from {trash} ({$percent})... restored = Restored {$items} {$items -> [one] item *[other] items diff --git a/src/app.rs b/src/app.rs index c3d341f..fe475f5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -48,7 +48,10 @@ use std::{ num::NonZeroU16, path::PathBuf, process, - sync::{Arc, Mutex}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, time::{self, Instant}, }; use tokio::sync::mpsc; @@ -306,7 +309,10 @@ pub enum Message { OpenWithSelection(usize), Paste(Option), PasteContents(PathBuf, ClipboardPaste), + PendingCancel(u64), + PendingCancelAll, PendingComplete(u64), + PendingDismiss, PendingError(u64, String), PendingProgress(u64, f32), Preview(Option), @@ -516,9 +522,10 @@ pub struct App { #[cfg(feature = "notify")] notification_opt: Option>>, pending_operation_id: u64, - pending_operations: BTreeMap, + pending_operations: BTreeMap)>, + progress_operations: usize, complete_operations: BTreeMap, - failed_operations: BTreeMap, + failed_operations: BTreeMap, search_id: widget::Id, #[cfg(feature = "wayland")] surface_ids: HashMap, @@ -694,7 +701,11 @@ impl App { fn operation(&mut self, operation: Operation) { let id = self.pending_operation_id; self.pending_operation_id += 1; - self.pending_operations.insert(id, (operation, 0.0)); + if operation.show_progress_notification() { + self.progress_operations += 1; + } + self.pending_operations + .insert(id, (operation, 0.0, Arc::new(AtomicBool::new(false)))); } fn remove_window(&mut self, id: &window::Id) { @@ -1209,12 +1220,20 @@ impl App { if !self.pending_operations.is_empty() { let mut section = widget::settings::section().title(fl!("pending")); - for (_id, (op, progress)) in self.pending_operations.iter().rev() { + for (id, (op, progress, _)) in self.pending_operations.iter().rev() { section = section.add(widget::column::with_children(vec![ - widget::text(op.pending_text()).into(), - widget::progress_bar(0.0..=100.0, *progress) - .height(progress_bar_height) - .into(), + widget::row::with_children(vec![ + widget::progress_bar(0.0..=100.0, *progress) + .height(progress_bar_height) + .into(), + widget::button::icon(widget::icon::from_name("window-close-symbolic")) + .on_press(Message::PendingCancel(*id)) + .padding(8) + .into(), + ]) + .align_y(Alignment::Center) + .into(), + widget::text(op.pending_text(*progress as i32)).into(), ])); } children.push(section.into()); @@ -1222,9 +1241,9 @@ impl App { if !self.failed_operations.is_empty() { let mut section = widget::settings::section().title(fl!("failed")); - for (_id, (op, error)) in self.failed_operations.iter().rev() { + for (_id, (op, progress, error)) in self.failed_operations.iter().rev() { section = section.add(widget::column::with_children(vec![ - widget::text(op.pending_text()).into(), + widget::text(op.pending_text(*progress as i32)).into(), widget::text(error).into(), ])); } @@ -1395,6 +1414,7 @@ impl Application for App { notification_opt: None, pending_operation_id: 0, pending_operations: BTreeMap::new(), + progress_operations: 0, complete_operations: BTreeMap::new(), failed_operations: BTreeMap::new(), search_id: widget::Id::unique(), @@ -2313,10 +2333,20 @@ impl Application for App { } } } + Message::PendingCancel(id) => { + if let Some((_, _, cancelled)) = self.pending_operations.get(&id) { + cancelled.store(true, Ordering::SeqCst); + } + } + Message::PendingCancelAll => { + for (_id, (_, _, cancelled)) in self.pending_operations.iter() { + cancelled.store(true, Ordering::SeqCst); + } + } Message::PendingComplete(id) => { let mut commands = Vec::with_capacity(3); - if let Some((op, _)) = self.pending_operations.remove(&id) { + if let Some((op, _, _)) = self.pending_operations.remove(&id) { if let Some(description) = op.toast() { if let Operation::Delete { ref paths } = op { let paths: Arc<[PathBuf]> = Arc::from(paths.as_slice()); @@ -2334,6 +2364,14 @@ impl Application for App { } self.complete_operations.insert(id, op); } + // Close progress notification if all relavent operations are finished + if !self + .pending_operations + .iter() + .any(|(_id, (op, _, _))| op.show_progress_notification()) + { + self.progress_operations = 0; + } // Potentially show a notification commands.push(self.update_notification()); // Manually rescan any trash tabs after any operation is completed @@ -2342,16 +2380,22 @@ impl Application for App { commands.push(self.search()); return Task::batch(commands); } + Message::PendingDismiss => { + self.progress_operations = 0; + } Message::PendingError(id, err) => { - if let Some((op, _)) = self.pending_operations.remove(&id) { - self.failed_operations.insert(id, (op, err)); - self.dialog_pages.push_back(DialogPage::FailedOperation(id)); + if let Some((op, progress, cancelled)) = self.pending_operations.remove(&id) { + self.failed_operations.insert(id, (op, progress, err)); + // Only show dialog if not cancelled + if !cancelled.load(Ordering::SeqCst) { + self.dialog_pages.push_back(DialogPage::FailedOperation(id)); + } } // Manually rescan any trash tabs after any operation is completed return self.rescan_trash(); } Message::PendingProgress(id, new_progress) => { - if let Some((_, progress)) = self.pending_operations.get_mut(&id) { + if let Some((_, progress, _)) = self.pending_operations.get_mut(&id) { *progress = new_progress; } return self.update_notification(); @@ -3316,7 +3360,7 @@ impl Application for App { ), DialogPage::FailedOperation(id) => { //TODO: try next dialog page (making sure index is used by Dialog messages)? - let (operation, err) = self.failed_operations.get(id)?; + let (operation, _, err) = self.failed_operations.get(id)?; //TODO: nice description of error widget::dialog() @@ -3747,6 +3791,80 @@ impl Application for App { Some(dialog.into()) } + fn footer(&self) -> Option> { + if self.progress_operations == 0 { + return None; + } + + let cosmic_theme::Spacing { + space_xxs, + space_xs, + space_s, + .. + } = theme::active().cosmic().spacing; + + let mut title = String::new(); + let mut total_progress = 0.0; + let mut count = 0; + for (_id, (op, progress, _)) in self.pending_operations.iter() { + if op.show_progress_notification() { + if title.is_empty() { + title = op.pending_text(*progress as i32); + } + total_progress += progress; + count += 1; + } + } + while count < self.progress_operations { + total_progress += 100.0; + count += 1; + } + total_progress /= count as f32; + if count > 1 { + title = fl!( + "operations-in-progress", + count = count, + percent = (total_progress as i32) + ); + } + + //TODO: get height from theme? + let progress_bar_height = Length::Fixed(4.0); + let progress_bar = + widget::progress_bar(0.0..=100.0, total_progress).height(progress_bar_height); + + let container = widget::layer_container(widget::column::with_children(vec![ + widget::row::with_children(vec![ + progress_bar.into(), + widget::button::icon(widget::icon::from_name("window-close-symbolic")) + .on_press(Message::PendingCancelAll) + .padding(8) + .into(), + ]) + .align_y(Alignment::Center) + .into(), + widget::text::body(title).into(), + widget::Space::with_height(space_s).into(), + widget::row::with_children(vec![ + widget::button::link(fl!("details")) + .on_press(Message::ToggleContextPage(ContextPage::EditHistory)) + .padding(0) + .trailing_icon(true) + .into(), + widget::horizontal_space().into(), + widget::button::standard(fl!("dismiss")) + .on_press(Message::PendingDismiss) + .into(), + ]) + .align_y(Alignment::Center) + .into(), + ])) + .padding([space_xxs, space_xs]) + .layer(cosmic_theme::Layer::Primary); + + Some(container.into()) + } + fn header_start(&self) -> Vec> { vec![menu::menu_bar( self.tab_model.active_data::(), @@ -3759,14 +3877,6 @@ impl Application for App { fn header_end(&self) -> Vec> { let mut elements = Vec::with_capacity(2); - if !self.pending_operations.is_empty() { - elements.push( - widget::button::text(format!("{}", self.pending_operations.len())) - .on_press(Message::ToggleContextPage(ContextPage::EditHistory)) - .into(), - ); - } - if let Some(term) = self.search_get() { if self.core.is_condensed() { elements.push( @@ -4188,15 +4298,16 @@ impl Application for App { } } - for (id, (pending_operation, _)) in self.pending_operations.iter() { + for (id, (pending_operation, _, cancelled)) in self.pending_operations.iter() { //TODO: use recipe? let id = *id; let pending_operation = pending_operation.clone(); + let cancelled = cancelled.clone(); subscriptions.push(Subscription::run_with_id( id, stream::channel(16, move |msg_tx| async move { let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); - match pending_operation.perform(id, &msg_tx).await { + match pending_operation.perform(id, &msg_tx, cancelled).await { Ok(()) => { let _ = msg_tx.lock().await.send(Message::PendingComplete(id)).await; } diff --git a/src/operation/mod.rs b/src/operation/mod.rs index bbb91ac..c73fb0c 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -4,7 +4,10 @@ use std::{ fs, io::{self, Read, Write}, path::{Path, PathBuf}, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use tokio::sync::{mpsc, Mutex}; use walkdir::WalkDir; @@ -134,6 +137,7 @@ async fn copy_or_move( moving: bool, id: u64, msg_tx: &Arc>>, + cancelled: Arc, ) -> Result<(), String> { let msg_tx = msg_tx.clone(); tokio::task::spawn_blocking(move || -> Result<(), String> { @@ -164,7 +168,7 @@ async fn copy_or_move( }) .collect(); - let mut context = Context::new(); + let mut context = Context::new(cancelled); { let msg_tx = msg_tx.clone(); @@ -308,38 +312,43 @@ fn paths_parent_name<'a>(paths: &'a Vec) -> Cow<'a, str> { } impl Operation { - pub fn pending_text(&self) -> String { + pub fn pending_text(&self, percent: i32) -> String { match self { Self::Compress { paths, to, .. } => fl!( "compressing", items = paths.len(), from = paths_parent_name(paths), - to = file_name(to) + to = file_name(to), + percent = percent ), Self::Copy { paths, to } => fl!( "copying", items = paths.len(), from = paths_parent_name(paths), - to = file_name(to) + to = file_name(to), + percent = percent ), Self::Delete { paths } => fl!( "moving", items = paths.len(), from = paths_parent_name(paths), - to = fl!("trash") + to = fl!("trash"), + percent = percent ), - Self::EmptyTrash => fl!("emptying-trash"), + Self::EmptyTrash => fl!("emptying-trash", percent = percent), Self::Extract { paths, to } => fl!( "extracting", items = paths.len(), from = paths_parent_name(paths), - to = file_name(to) + to = file_name(to), + percent = percent ), Self::Move { paths, to } => fl!( "moving", items = paths.len(), from = paths_parent_name(paths), - to = file_name(to) + to = file_name(to), + percent = percent ), Self::NewFile { path } => fl!( "creating", @@ -354,7 +363,7 @@ impl Operation { Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } - Self::Restore { paths } => fl!("restoring", items = paths.len()), + Self::Restore { paths } => fl!("restoring", items = paths.len(), percent = percent), Self::SetExecutableAndLaunch { path } => { fl!("setting-executable-and-launching", name = file_name(path)) } @@ -412,6 +421,23 @@ impl Operation { } } + pub fn show_progress_notification(&self) -> bool { + // Long running operations show a progress notification + match self { + Self::Compress { .. } + | Self::Copy { .. } + | Self::Delete { .. } + | Self::EmptyTrash + | Self::Extract { .. } + | Self::Move { .. } + | Self::Restore { .. } => true, + Self::NewFile { .. } + | Self::NewFolder { .. } + | Self::Rename { .. } + | Self::SetExecutableAndLaunch { .. } => false, + } + } + pub fn toast(&self) -> Option { match self { Self::Compress { .. } => Some(self.completed_text()), @@ -427,6 +453,7 @@ impl Operation { self, id: u64, msg_tx: &Arc>>, + cancelled: Arc, ) -> Result<(), String> { let _ = msg_tx .lock() @@ -435,7 +462,6 @@ impl Operation { .await; //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE - //TODO: SAFELY HANDLE CANCEL match self { Self::Compress { paths, @@ -471,6 +497,10 @@ impl Operation { let total_paths = paths.len(); for (i, path) in paths.iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + executor::block_on(async { let total_progress = (i as f32) / total_paths as f32; let _ = msg_tx @@ -493,15 +523,16 @@ impl Operation { } ArchiveType::Zip => { let mut archive = fs::File::create(&to) - .map(io::BufWriter::new) .map(zip::ZipWriter::new) .map_err(err_str)?; - //TODO: set unix_permissions per file? - let zip_options = zip::write::SimpleFileOptions::default(); - let total_paths = paths.len(); + let mut buffer = vec![0; 4 * 1024 * 1024]; for (i, path) in paths.iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + executor::block_on(async { let total_progress = (i as f32) / total_paths as f32; let _ = msg_tx @@ -511,21 +542,54 @@ impl Operation { .await; }); + let mut zip_options = zip::write::SimpleFileOptions::default(); if let Some(relative_path) = path.strip_prefix(relative_root).map_err(err_str)?.to_str() { if path.is_file() { + let mut file = fs::File::open(&path).map_err(err_str)?; + let metadata = file.metadata().map_err(err_str)?; + let total = metadata.len(); + if total >= 4 * 1024 * 1024 * 1024 { + // The large file option must be enabled for files above 4 GiB + zip_options = zip_options.large_file(true); + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let mode = metadata.mode(); + zip_options = zip_options.unix_permissions(mode); + } archive .start_file(relative_path, zip_options) .map_err(err_str)?; + let mut current = 0; + loop { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } - let mut buffer = Vec::new(); - let mut file = fs::File::open(&path) - .map(io::BufReader::new) - .map_err(err_str)?; + let count = file.read(&mut buffer).map_err(err_str)?; + if count == 0 { + break; + } + archive.write_all(&buffer[..count]).map_err(err_str)?; + current += count; - file.read_to_end(&mut buffer).map_err(err_str)?; - archive.write_all(&buffer).map_err(err_str)?; + executor::block_on(async { + let file_progress = current as f32 / total as f32; + let total_progress = + (i as f32 + file_progress) / total_paths as f32; + let _ = msg_tx + .lock() + .await + .send(Message::PendingProgress( + id, + 100.0 * total_progress, + )) + .await; + }); + } } else { archive .add_directory(relative_path, zip_options) @@ -545,26 +609,29 @@ impl Operation { .map_err(err_str)?; } Self::Copy { paths, to } => { - copy_or_move(paths, to, false, id, msg_tx).await?; + copy_or_move(paths, to, false, id, msg_tx, cancelled).await?; } Self::Delete { paths } => { let total = paths.len(); - let mut count = 0; - for path in paths { - let items_opt = tokio::task::spawn_blocking(|| trash::delete(path)) - .await - .map_err(err_str)? - .map_err(err_str)?; - //TODO: items_opt allows for easy restore - count += 1; + for (i, path) in paths.into_iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + let _ = msg_tx .lock() .await .send(Message::PendingProgress( id, - 100.0 * (count as f32) / (total as f32), + 100.0 * (i as f32) / (total as f32), )) .await; + + let _items_opt = tokio::task::spawn_blocking(|| trash::delete(path)) + .await + .map_err(err_str)? + .map_err(err_str)?; + //TODO: items_opt allows for easy restore } } Self::EmptyTrash => { @@ -578,25 +645,41 @@ impl Operation { ) ))] { - tokio::task::spawn_blocking(|| { - let items = trash::os_limited::list()?; - trash::os_limited::purge_all(items) + let msg_tx = msg_tx.clone(); + tokio::task::spawn_blocking(move || -> Result<(), String> { + let items = trash::os_limited::list().map_err(err_str)?; + let count = items.len(); + for (i, item) in items.into_iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + + executor::block_on(async { + let total_progress = i as f32 / count as f32; + let _ = msg_tx + .lock() + .await + .send(Message::PendingProgress(id, 100.0 * total_progress)) + .await; + }); + + trash::os_limited::purge_all([item]).map_err(err_str)?; + } + Ok(()) }) .await - .map_err(err_str)? - .map_err(err_str)?; + .map_err(err_str)??; } - let _ = msg_tx - .lock() - .await - .send(Message::PendingProgress(id, 100.0)) - .await; } Self::Extract { paths, to } => { let msg_tx = msg_tx.clone(); tokio::task::spawn_blocking(move || -> Result<(), String> { let total_paths = paths.len(); for (i, path) in paths.iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + executor::block_on(async { let total_progress = (i as f32) / total_paths as f32; let _ = msg_tx @@ -618,6 +701,7 @@ impl Operation { } } + //TODO: support cancellation while extracting! let mime = mime_for_path(&path); match mime.essence_str() { "application/gzip" | "application/x-compressed-tar" => { @@ -669,40 +753,37 @@ impl Operation { .map_err(err_str)?; } Self::Move { paths, to } => { - copy_or_move(paths, to, true, id, msg_tx).await?; + copy_or_move(paths, to, true, id, msg_tx, cancelled).await?; } Self::NewFolder { path } => { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + tokio::task::spawn_blocking(|| fs::create_dir(path)) .await .map_err(err_str)? .map_err(err_str)?; - let _ = msg_tx - .lock() - .await - .send(Message::PendingProgress(id, 100.0)) - .await; } Self::NewFile { path } => { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + tokio::task::spawn_blocking(|| fs::File::create(path)) .await .map_err(err_str)? .map_err(err_str)?; - let _ = msg_tx - .lock() - .await - .send(Message::PendingProgress(id, 100.0)) - .await; } Self::Rename { from, to } => { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + tokio::task::spawn_blocking(|| fs::rename(from, to)) .await .map_err(err_str)? .map_err(err_str)?; - let _ = msg_tx - .lock() - .await - .send(Message::PendingProgress(id, 100.0)) - .await; } #[cfg(target_os = "macos")] Self::Restore { .. } => { @@ -712,49 +793,56 @@ impl Operation { #[cfg(not(target_os = "macos"))] Self::Restore { paths } => { let total = paths.len(); - let mut count = 0; - for path in paths { - tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path])) - .await - .map_err(err_str)? - .map_err(err_str)?; - count += 1; + for (i, path) in paths.into_iter().enumerate() { + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + let _ = msg_tx .lock() .await .send(Message::PendingProgress( id, - 100.0 * (count as f32) / (total as f32), + 100.0 * (i as f32) / (total as f32), )) .await; + + tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path])) + .await + .map_err(err_str)? + .map_err(err_str)?; } } Self::SetExecutableAndLaunch { path } => { - tokio::task::spawn_blocking(move || -> io::Result<()> { + tokio::task::spawn_blocking(move || -> Result<(), String> { //TODO: what to do on non-Unix systems? #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&path)?.permissions(); + + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); + } + + let mut perms = fs::metadata(&path).map_err(err_str)?.permissions(); let current_mode = perms.mode(); let new_mode = current_mode | 0o111; perms.set_mode(new_mode); - fs::set_permissions(&path, perms)?; + fs::set_permissions(&path, perms).map_err(err_str)?; + } + + if cancelled.load(Ordering::SeqCst) { + return Err(fl!("cancelled")); } let mut command = std::process::Command::new(path); - spawn_detached(&mut command)?; + spawn_detached(&mut command).map_err(err_str)?; Ok(()) }) .await .map_err(err_str)? .map_err(err_str)?; - let _ = msg_tx - .lock() - .await - .send(Message::PendingProgress(id, 100.0)) - .await; } } diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index 2116a2e..95d25a5 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -4,22 +4,29 @@ use std::{ 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, + cancelled: Arc, on_progress: Box, on_replace: Box ReplaceResult + 'static>, replace_result_opt: Option, } impl Context { - pub fn new() -> Self { + pub fn new(cancelled: Arc) -> 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, @@ -34,12 +41,20 @@ impl Context { 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) })?; @@ -91,6 +106,10 @@ impl Context { 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, @@ -215,6 +234,10 @@ impl Op { .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;