Implement progress notification and cancellation
This commit is contained in:
parent
5c7cf52b25
commit
00ed3115cc
4 changed files with 336 additions and 108 deletions
|
|
@ -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
|
||||
|
|
|
|||
165
src/app.rs
165
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<Entity>),
|
||||
PasteContents(PathBuf, ClipboardPaste),
|
||||
PendingCancel(u64),
|
||||
PendingCancelAll,
|
||||
PendingComplete(u64),
|
||||
PendingDismiss,
|
||||
PendingError(u64, String),
|
||||
PendingProgress(u64, f32),
|
||||
Preview(Option<Entity>),
|
||||
|
|
@ -516,9 +522,10 @@ pub struct App {
|
|||
#[cfg(feature = "notify")]
|
||||
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
|
||||
pending_operation_id: u64,
|
||||
pending_operations: BTreeMap<u64, (Operation, f32)>,
|
||||
pending_operations: BTreeMap<u64, (Operation, f32, Arc<AtomicBool>)>,
|
||||
progress_operations: usize,
|
||||
complete_operations: BTreeMap<u64, Operation>,
|
||||
failed_operations: BTreeMap<u64, (Operation, String)>,
|
||||
failed_operations: BTreeMap<u64, (Operation, f32, String)>,
|
||||
search_id: widget::Id,
|
||||
#[cfg(feature = "wayland")]
|
||||
surface_ids: HashMap<WlOutput, WindowId>,
|
||||
|
|
@ -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<Element<Message>> {
|
||||
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<Element<Self::Message>> {
|
||||
vec![menu::menu_bar(
|
||||
self.tab_model.active_data::<Tab>(),
|
||||
|
|
@ -3759,14 +3877,6 @@ impl Application for App {
|
|||
fn header_end(&self) -> Vec<Element<Self::Message>> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Mutex<Sender<Message>>>,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> 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<PathBuf>) -> 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<String> {
|
||||
match self {
|
||||
Self::Compress { .. } => Some(self.completed_text()),
|
||||
|
|
@ -427,6 +453,7 @@ impl Operation {
|
|||
self,
|
||||
id: u64,
|
||||
msg_tx: &Arc<Mutex<Sender<Message>>>,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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() -> Self {
|
||||
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,
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue