1use crate::app::{ArchiveType, DialogPage, Message, REPLACE_BUTTON_ID};
2use crate::config::IconSizes;
3use crate::spawn_detached::spawn_detached;
4use crate::{archive, fl, tab};
5use cosmic::iced::futures::channel::mpsc::Sender;
6use cosmic::iced::futures::{self, SinkExt, StreamExt, stream};
7use std::borrow::Cow;
8use std::fmt::Formatter;
9use std::fs;
10use std::io::{self, Read, Write};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use tokio::sync::{Mutex as TokioMutex, mpsc};
14use walkdir::WalkDir;
15use zip::AesMode::Aes256;
16
17pub use self::controller::{Controller, ControllerState};
18pub mod controller;
19
20pub use notifiers::*;
21mod notifiers;
22
23pub use self::reader::OpReader;
24pub mod reader;
25
26use self::recursive::{Context, Method};
27pub mod recursive;
28
29async fn handle_replace(
30 msg_tx: Arc<TokioMutex<Sender<Message>>>,
31 file_from: PathBuf,
32 file_to: PathBuf,
33 multiple: bool,
34 conflict_count: usize,
35) -> ReplaceResult {
36 let item_from = match tab::item_from_path(file_from, IconSizes::default()) {
37 Ok(ok) => Box::new(ok),
38 Err(err) => {
39 log::warn!("{err}");
40 return ReplaceResult::Cancel;
41 }
42 };
43
44 let item_to = match tab::item_from_path(file_to, IconSizes::default()) {
45 Ok(ok) => Box::new(ok),
46 Err(err) => {
47 log::warn!("{err}");
48 return ReplaceResult::Cancel;
49 }
50 };
51
52 let (tx, mut rx) = mpsc::channel(1);
53 let _ = msg_tx
54 .lock()
55 .await
56 .send(Message::DialogPush(
57 DialogPage::Replace {
58 from: item_from,
59 to: item_to,
60 multiple,
61 apply_to_all: false,
62 conflict_count,
63 tx,
64 },
65 Some(REPLACE_BUTTON_ID.clone()),
66 ))
67 .await;
68 rx.recv().await.unwrap_or(ReplaceResult::Cancel)
69}
70
71fn get_directory_name(file_name: &str) -> &str {
72 for ext in crate::archive::SUPPORTED_EXTENSIONS {
74 if let Some(stripped) = file_name.strip_suffix(ext) {
75 return stripped;
76 }
77 }
78 file_name
79}
80
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
82pub enum ReplaceResult {
83 Replace(bool),
84 KeepBoth,
85 Skip(bool),
86 Cancel,
87}
88
89async fn copy_or_move(
90 paths: Vec<PathBuf>,
91 to: PathBuf,
92 method: Method,
93 msg_tx: &Arc<TokioMutex<Sender<Message>>>,
94 controller: Controller,
95) -> Result<OperationSelection, OperationError> {
96 let msg_tx = msg_tx.clone();
97 let controller_c = controller.clone();
98
99 compio::runtime::spawn(async move {
100 let controller = controller_c;
101 log::info!(
102 "{} {:?} to {}",
103 match method {
104 Method::Copy => "Copy",
105 Method::Move { .. } => "Move",
106 },
107 paths,
108 to.display()
109 );
110
111 let from_to_pairs_iter = paths
113 .into_iter()
114 .zip(std::iter::repeat(to.as_path()))
115 .filter_map(|(from, to)| {
116 if matches!(from.parent(), Some(parent) if parent == to)
117 && matches!(method, Method::Copy)
118 {
119 let to = copy_unique_path(&from, to);
122 Some((from, to))
123 } else if let Some(name) = from.file_name() {
124 let to = to.join(name);
125 Some((from, to))
126 } else {
127 None
129 }
130 });
131
132 let from_to_pairs: Vec<(PathBuf, PathBuf)> = if matches!(method, Method::Move { .. }) {
136 from_to_pairs_iter
137 .map(|(from, to)| async move {
138 if to.exists() {
140 return Some((from, to));
141 }
142
143 match compio::fs::rename(&from, &to).await {
144 Ok(()) => {
145 log::info!("renamed {} to {}", from.display(), to.display());
146 None
147 }
148 Err(err) => {
149 log::info!(
150 "failed to rename {} to {}, fallback to recursive move: {}",
151 from.display(),
152 to.display(),
153 err
154 );
155 Some((from, to))
156 }
157 }
158 })
159 .collect::<cosmic::iced::futures::stream::FuturesOrdered<_>>()
160 .fold(Vec::new(), |mut pairs, pair| async move {
161 if let Some(pair) = pair {
162 pairs.push(pair);
163 }
164 pairs
165 })
166 .await
167 } else {
168 from_to_pairs_iter.collect()
169 };
170
171 let mut context = Context::new(controller.clone());
172
173 {
174 let controller = controller.clone();
175 context = context.on_progress(move |_op, progress| {
176 let item_progress = match progress.total_bytes {
177 Some(total_bytes) => {
178 if total_bytes == 0 {
179 1.0
180 } else {
181 progress.current_bytes as f32 / total_bytes as f32
182 }
183 }
184 None => 0.0,
185 };
186 let total_progress =
187 (item_progress + progress.current_ops as f32) / progress.total_ops as f32;
188 controller.set_progress(total_progress);
189 });
190 }
191
192 {
193 let msg_tx = msg_tx.clone();
194 context = context.on_replace(move |op, conflict_count| {
195 let msg_tx = msg_tx.clone();
196 Box::pin(handle_replace(
197 msg_tx,
198 op.from.clone(),
199 op.to.clone(),
200 true,
201 conflict_count,
202 ))
203 });
204 }
205
206 context
207 .recursive_copy_or_move(from_to_pairs, method)
208 .await?;
209
210 Result::<OperationSelection, OperationError>::Ok(context.op_sel)
211 })
212 .await
213 .map_err(wrap_compio_spawn_error)?
214}
215
216pub async fn sync_to_disk(
217 written_files: Vec<PathBuf>,
218 target_dirs: std::collections::HashSet<PathBuf>,
219) {
220 stream::iter(written_files.into_iter().map(|path| async move {
222 if let Ok(file) = compio::fs::OpenOptions::new().write(true).open(&path).await {
223 let _ = file.sync_all().await;
224 }
225 }))
226 .buffer_unordered(32)
227 .collect::<()>()
228 .await;
229
230 stream::iter(target_dirs.into_iter().map(|path| async move {
232 if let Ok(dir) = compio::fs::OpenOptions::new().read(true).open(&path).await {
233 let _ = dir.sync_all().await;
234 }
235 }))
236 .buffer_unordered(16)
237 .collect::<()>()
238 .await;
239}
240
241pub fn copy_unique_path(from: &Path, to: &Path) -> PathBuf {
242 const COMPOUND_EXTENSIONS: &[&str] = &[
244 ".tar.gz",
245 ".tar.bz2",
246 ".tar.xz",
247 ".tar.zst",
248 ".tar.lz",
249 ".tar.lzma",
250 ".tar.sz",
251 ".tar.lzo",
252 ".tar.br",
253 ".tar.Z",
254 ".tar.pz",
255 ];
256
257 let mut to = to.to_owned();
258 if let Some(file_name) = from.file_name().and_then(|name| name.to_str()) {
259 let (stem, ext) = if from.is_dir() {
260 (file_name.to_string(), None)
261 } else {
262 let file_name = file_name.to_string();
263 COMPOUND_EXTENSIONS
264 .iter()
265 .copied()
266 .find(|&ext| file_name.ends_with(ext))
267 .map(|ext| {
268 (
269 file_name.strip_suffix(ext).unwrap().to_string(),
270 Some(ext[1..].to_string()),
271 )
272 })
273 .unwrap_or_else(|| {
274 from.file_stem()
275 .and_then(|s| s.to_str())
276 .map_or((file_name, None), |stem| {
277 (
278 stem.to_string(),
279 from.extension()
280 .and_then(|e| e.to_str())
281 .map(str::to_string),
282 )
283 })
284 })
285 };
286
287 for n in 0.. {
288 let new_name = if n == 0 {
289 file_name.to_string()
290 } else {
291 match ext {
292 Some(ref ext) => format!("{} ({} {}).{}", stem, fl!("copy_noun"), n, ext),
293 None => format!("{} ({} {})", stem, fl!("copy_noun"), n),
294 }
295 };
296
297 to.push(&new_name);
298
299 if !matches!(to.try_exists(), Ok(true)) {
300 break;
301 }
302 to.pop();
304 }
305 }
306 to
307}
308
309fn file_name(path: &Path) -> Cow<'_, str> {
310 path.file_name()
311 .map_or_else(|| fl!("unknown-folder").into(), |x| x.to_string_lossy())
312}
313
314fn parent_name(path: &Path) -> Cow<'_, str> {
315 let Some(parent) = path.parent() else {
316 return fl!("unknown-folder").into();
317 };
318
319 file_name(parent)
320}
321
322fn paths_parent_name(paths: &[PathBuf]) -> Cow<'_, str> {
323 let Some(first_path) = paths.first() else {
324 return fl!("unknown-folder").into();
325 };
326
327 let Some(parent) = first_path.parent() else {
328 return fl!("unknown-folder").into();
329 };
330
331 for path in paths {
332 if path.parent() != Some(parent) {
334 return fl!("unknown-folder").into();
335 }
336 }
337
338 file_name(parent)
339}
340
341#[derive(Clone, Debug, Default)]
342pub struct OperationSelection {
343 pub ignored: Vec<PathBuf>,
345 pub selected: Vec<PathBuf>,
347}
348
349#[derive(Clone, Debug, Eq, Hash, PartialEq)]
350pub enum Operation {
351 Compress {
353 paths: Vec<PathBuf>,
354 to: PathBuf,
355 archive_type: ArchiveType,
356 password: Option<String>,
357 },
358 Copy {
360 paths: Vec<PathBuf>,
361 to: PathBuf,
362 },
363 Delete {
365 paths: Vec<PathBuf>,
366 },
367 DeleteTrash {
369 items: Vec<trash::TrashItem>,
370 },
371 EmptyTrash,
373 Extract {
375 paths: Box<[PathBuf]>,
376 to: PathBuf,
377 password: Option<String>,
378 },
379 Move {
381 paths: Vec<PathBuf>,
382 to: PathBuf,
383 cross_device_copy: bool,
384 },
385 NewFile {
386 path: PathBuf,
387 },
388 NewFolder {
389 path: PathBuf,
390 },
391 PermanentlyDelete {
393 paths: Box<[PathBuf]>,
394 },
395 RemoveFromRecents {
396 paths: Box<[PathBuf]>,
397 },
398 Rename {
399 from: PathBuf,
400 to: PathBuf,
401 },
402 Restore {
404 items: Vec<trash::TrashItem>,
405 },
406 SetExecutableAndLaunch {
408 path: PathBuf,
409 },
410 SetPermissions {
412 path: PathBuf,
413 mode: u32,
414 },
415}
416
417#[derive(Clone, Debug)]
418pub enum OperationErrorType {
419 Generic(String),
420 PasswordRequired,
421}
422#[derive(Clone, Debug)]
423pub struct OperationError {
424 pub kind: OperationErrorType,
425}
426
427impl OperationError {
428 pub fn from_state(state: ControllerState, controller: &Controller) -> Self {
429 let message = if state == ControllerState::Failed {
430 controller.set_state(ControllerState::Failed);
431 fl!("failed")
432 } else {
433 controller.cancel();
434 fl!("cancelled")
435 };
436
437 Self {
438 kind: OperationErrorType::Generic(message),
439 }
440 }
441
442 pub fn from_err<T: ToString>(err: T, controller: &Controller) -> Self {
443 controller.set_state(ControllerState::Failed);
444
445 Self {
446 kind: OperationErrorType::Generic(err.to_string()),
447 }
448 }
449
450 pub fn from_kind(kind: OperationErrorType, controller: &Controller) -> Self {
451 controller.set_state(ControllerState::Failed);
452 Self { kind }
453 }
454
455 pub fn from_msg(m: impl Into<String>) -> Self {
456 Self {
457 kind: OperationErrorType::Generic(m.into()),
458 }
459 }
460}
461
462impl std::error::Error for OperationError {}
463
464impl std::fmt::Display for OperationError {
465 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
466 match &self.kind {
467 OperationErrorType::Generic(s) => s.fmt(f),
468 OperationErrorType::PasswordRequired => f.write_str("Password required"),
469 }
470 }
471}
472
473impl Operation {
474 pub fn pending_text(&self, ratio: f32, state: ControllerState) -> String {
475 let percent = (ratio * 100.0) as i32;
476 let progress = || match state {
477 ControllerState::Running => fl!("progress", percent = percent),
478 ControllerState::Paused => fl!("progress-paused", percent = percent),
479 ControllerState::Cancelled => fl!("progress-cancelled", percent = percent),
480 ControllerState::Failed => fl!("progress-failed", percent = percent),
481 };
482 match self {
483 Self::Compress { paths, to, .. } => fl!(
484 "compressing",
485 items = paths.len(),
486 from = paths_parent_name(paths),
487 to = file_name(to),
488 progress = progress()
489 ),
490 Self::Copy { paths, to } => fl!(
491 "copying",
492 items = paths.len(),
493 from = paths_parent_name(paths),
494 to = file_name(to),
495 progress = progress()
496 ),
497 Self::Delete { paths } => fl!(
498 "moving",
499 items = paths.len(),
500 from = paths_parent_name(paths),
501 to = fl!("trash"),
502 progress = progress()
503 ),
504 Self::DeleteTrash { items } => {
505 fl!("deleting", items = items.len(), progress = progress())
506 }
507 Self::EmptyTrash => fl!("emptying-trash", progress = progress()),
508 Self::Extract {
509 paths,
510 to,
511 password: _,
512 } => fl!(
513 "extracting",
514 items = paths.len(),
515 from = paths_parent_name(paths),
516 to = file_name(to),
517 progress = progress()
518 ),
519 Self::Move { paths, to, .. } => fl!(
520 "moving",
521 items = paths.len(),
522 from = paths_parent_name(paths),
523 to = file_name(to),
524 progress = progress()
525 ),
526 Self::NewFile { path } => fl!(
527 "creating",
528 name = file_name(path),
529 parent = parent_name(path)
530 ),
531 Self::NewFolder { path } => fl!(
532 "creating",
533 name = file_name(path),
534 parent = parent_name(path)
535 ),
536 Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()),
537 Self::Rename { from, to } => {
538 fl!("renaming", from = file_name(from), to = file_name(to))
539 }
540 Self::RemoveFromRecents { paths } => fl!("removing-from-recents", items = paths.len()),
541 Self::Restore { items } => fl!("restoring", items = items.len(), progress = progress()),
542 Self::SetExecutableAndLaunch { path } => {
543 fl!("setting-executable-and-launching", name = file_name(path))
544 }
545 Self::SetPermissions { path, mode } => {
546 fl!(
547 "setting-permissions",
548 name = file_name(path),
549 mode = format!("{:#03o}", mode)
550 )
551 }
552 }
553 }
554
555 pub fn completed_text(&self) -> String {
556 match self {
557 Self::Compress { paths, to, .. } => fl!(
558 "compressed",
559 items = paths.len(),
560 from = paths_parent_name(paths),
561 to = file_name(to)
562 ),
563 Self::Copy { paths, to } => fl!(
564 "copied",
565 items = paths.len(),
566 from = paths_parent_name(paths),
567 to = file_name(to)
568 ),
569 Self::Delete { paths } => fl!(
570 "moved",
571 items = paths.len(),
572 from = paths_parent_name(paths),
573 to = fl!("trash")
574 ),
575 Self::DeleteTrash { items } => fl!("deleted", items = items.len()),
576 Self::EmptyTrash => fl!("emptied-trash"),
577 Self::Extract {
578 paths,
579 to,
580 password: _,
581 } => fl!(
582 "extracted",
583 items = paths.len(),
584 from = paths_parent_name(paths),
585 to = file_name(to)
586 ),
587 Self::Move { paths, to, .. } => fl!(
588 "moved",
589 items = paths.len(),
590 from = paths_parent_name(paths),
591 to = file_name(to)
592 ),
593 Self::NewFile { path } => fl!(
594 "created",
595 name = file_name(path),
596 parent = parent_name(path)
597 ),
598 Self::NewFolder { path } => fl!(
599 "created",
600 name = file_name(path),
601 parent = parent_name(path)
602 ),
603 Self::PermanentlyDelete { paths } => fl!("permanently-deleted", items = paths.len()),
604 Self::RemoveFromRecents { paths } => fl!("removed-from-recents", items = paths.len()),
605 Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)),
606 Self::Restore { items } => fl!("restored", items = items.len()),
607 Self::SetExecutableAndLaunch { path } => {
608 fl!("set-executable-and-launched", name = file_name(path))
609 }
610 Self::SetPermissions { path, mode } => {
611 fl!(
612 "set-permissions",
613 name = file_name(path),
614 mode = format!("{:#03o}", mode)
615 )
616 }
617 }
618 }
619
620 pub const fn show_progress_notification(&self) -> bool {
621 match self {
623 Self::Compress { .. }
624 | Self::Copy { .. }
625 | Self::Delete { .. }
626 | Self::DeleteTrash { .. }
627 | Self::EmptyTrash
628 | Self::Extract { .. }
629 | Self::Move { .. }
630 | Self::PermanentlyDelete { .. }
631 | Self::Restore { .. } => true,
632 Self::NewFile { .. }
633 | Self::NewFolder { .. }
634 | Self::RemoveFromRecents { .. }
635 | Self::Rename { .. }
636 | Self::SetExecutableAndLaunch { .. }
637 | Self::SetPermissions { .. } => false,
638 }
639 }
640
641 pub fn toast(&self) -> Option<String> {
642 match self {
643 Self::Compress { .. } => Some(self.completed_text()),
644 Self::Delete { .. } => Some(self.completed_text()),
645 Self::Extract { .. } => Some(self.completed_text()),
646 _ => None,
648 }
649 }
650
651 pub async fn perform(
653 self,
654 msg_tx: &Arc<TokioMutex<Sender<Message>>>,
655 controller: Controller,
656 ) -> Result<OperationSelection, OperationError> {
657 let controller_clone = controller.clone();
658
659 let paths: Result<OperationSelection, OperationError> = match self {
661 Self::Compress {
662 paths,
663 to,
664 archive_type,
665 password,
666 } => {
667 let controller_c = controller.clone();
668 compio::runtime::spawn_blocking(
669 move || -> Result<OperationSelection, OperationError> {
670 let controller = controller_c;
671 let Some(relative_root) = to.parent() else {
672 return Err(OperationError::from_err(
673 format!("path {} has no parent directory", to.display()),
674 &controller,
675 ));
676 };
677
678 let op_sel = OperationSelection {
679 ignored: paths.clone(),
680 selected: vec![to.clone()],
681 };
682
683 let mut paths = paths;
684 for path in &paths.clone() {
685 if path.is_dir() {
686 let new_paths_it = WalkDir::new(path).into_iter();
687 for entry in new_paths_it.skip(1) {
688 let entry = entry
689 .map_err(|e| OperationError::from_err(e, &controller))?;
690 paths.push(entry.into_path());
691 }
692 }
693 }
694
695 match archive_type {
696 ArchiveType::Tgz => {
697 let mut archive = fs::File::create(&to)
698 .map(io::BufWriter::new)
699 .map(|w| {
700 flate2::write::GzEncoder::new(
701 w,
702 flate2::Compression::default(),
703 )
704 })
705 .map(tar::Builder::new)
706 .map_err(|e| OperationError::from_err(e, &controller))?;
707
708 let total_paths = paths.len();
709 for (i, path) in paths.iter().enumerate() {
710 futures::executor::block_on(async {
711 controller
712 .check()
713 .await
714 .map_err(|e| OperationError::from_state(e, &controller))
715 })?;
716
717 controller.set_progress((i as f32) / total_paths as f32);
718
719 if let Some(relative_path) = path
720 .strip_prefix(relative_root)
721 .map_err(|e| OperationError::from_err(e, &controller))?
722 .to_str()
723 {
724 archive
725 .append_path_with_name(path, relative_path)
726 .map_err(|e| {
727 OperationError::from_err(e, &controller)
728 })?;
729 }
730 }
731
732 archive
733 .finish()
734 .map_err(|e| OperationError::from_err(e, &controller))?;
735 }
736 ArchiveType::Zip => {
737 let mut archive = fs::File::create(&to)
738 .map(io::BufWriter::new)
739 .map(zip::ZipWriter::new)
740 .map_err(|e| OperationError::from_err(e, &controller))?;
741
742 let total_paths = paths.len();
743 let mut buffer = vec![0; 4 * 1024 * 1024];
744 for (i, path) in paths.iter().enumerate() {
745 futures::executor::block_on(async {
746 controller
747 .check()
748 .await
749 .map_err(|s| OperationError::from_state(s, &controller))
750 })?;
751
752 controller.set_progress((i as f32) / total_paths as f32);
753
754 let mut zip_options = zip::write::SimpleFileOptions::default();
755 if password.is_some() {
756 zip_options = zip_options.with_aes_encryption(
757 Aes256,
758 password.as_deref().unwrap(),
759 );
760 }
761 if let Some(relative_path) = path
762 .strip_prefix(relative_root)
763 .map_err(|e| OperationError::from_err(e, &controller))?
764 .to_str()
765 {
766 let mut file = fs::File::open(path).map_err(|e| {
767 OperationError::from_err(e, &controller)
768 })?;
769 let metadata = file.metadata().map_err(|e| {
770 OperationError::from_err(e, &controller)
771 })?;
772
773 if let Ok(modified) = metadata.modified()
774 && let Some(last_modified) =
775 archive::system_time_to_zip_date_time(modified)
776 {
777 zip_options =
778 zip_options.last_modified_time(last_modified);
779 }
780
781 #[cfg(unix)]
782 {
783 use std::os::unix::fs::MetadataExt;
784 let mode = metadata.mode();
785 zip_options = zip_options.unix_permissions(mode);
786 }
787
788 if path.is_file() {
789 let total = metadata.len();
790 if total >= 4 * 1024 * 1024 * 1024 {
791 zip_options = zip_options.large_file(true);
793 }
794 archive
795 .start_file(relative_path, zip_options)
796 .map_err(|e| {
797 OperationError::from_err(e, &controller)
798 })?;
799 let mut current = 0;
800 loop {
801 futures::executor::block_on(async {
802 controller.check().await.map_err(|s| {
803 OperationError::from_state(s, &controller)
804 })
805 })?;
806
807 let count =
808 file.read(&mut buffer).map_err(|e| {
809 OperationError::from_err(e, &controller)
810 })?;
811 if count == 0 {
812 break;
813 }
814 archive.write_all(&buffer[..count]).map_err(
815 |e| OperationError::from_err(e, &controller),
816 )?;
817 current += count;
818
819 let file_progress = current as f32 / total as f32;
820 let total_progress =
821 (i as f32 + file_progress) / total_paths as f32;
822 controller.set_progress(total_progress);
823 }
824 } else {
825 archive
826 .add_directory(relative_path, zip_options)
827 .map_err(|e| {
828 OperationError::from_err(e, &controller)
829 })?;
830 }
831 }
832 }
833
834 archive
835 .finish()
836 .map_err(|e| OperationError::from_err(e, &controller))?;
837 }
838 }
839
840 Ok(op_sel)
841 },
842 )
843 .await
844 .map_err(wrap_compio_spawn_error)?
845 }
846 Self::Copy { paths, to } => {
847 copy_or_move(paths, to, Method::Copy, msg_tx, controller).await
848 }
849 Self::Delete { paths } => {
850 let total = paths.len();
851 for (i, path) in paths.into_iter().enumerate() {
852 futures::executor::block_on(async {
853 controller
854 .check()
855 .await
856 .map_err(|s| OperationError::from_state(s, &controller))
857 })?;
858
859 controller.set_progress((i as f32) / (total as f32));
860
861 let _items_opt = compio::runtime::spawn_blocking(|| trash::delete(path))
862 .await
863 .map_err(wrap_compio_spawn_error)?;
864 }
866 Ok(OperationSelection::default())
867 }
868 Self::DeleteTrash { items } => {
869 #[cfg(any(
870 target_os = "windows",
871 all(
872 unix,
873 not(target_os = "macos"),
874 not(target_os = "ios"),
875 not(target_os = "android")
876 )
877 ))]
878 {
879 let controller_clone = controller.clone();
880 compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {
881 let controller = controller_clone;
882 let count = items.len();
883 for (i, item) in items.into_iter().enumerate() {
884 futures::executor::block_on(async {
885 controller
886 .check()
887 .await
888 .map_err(|s| OperationError::from_state(s, &controller))
889 })?;
890
891 controller.set_progress(i as f32 / count as f32);
892
893 trash::os_limited::purge_all([item])
894 .map_err(|e| OperationError::from_err(e, &controller))?;
895 }
896 Ok(())
897 })
898 .await
899 .map_err(wrap_compio_spawn_error)?
900 .map_err(|e| OperationError::from_err(e, &controller))?;
901 }
902 Ok(OperationSelection::default())
903 }
904 Self::EmptyTrash => {
905 #[cfg(any(
906 target_os = "windows",
907 all(
908 unix,
909 not(target_os = "macos"),
910 not(target_os = "ios"),
911 not(target_os = "android")
912 )
913 ))]
914 {
915 let controller_clone = controller.clone();
916 compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {
917 let controller = controller_clone;
918 let items = trash::os_limited::list()
919 .map_err(|e| OperationError::from_err(e, &controller))?;
920 let count = items.len();
921 let mut errors: Vec<trash::Error> = Vec::new();
922
923 for (i, item) in items.into_iter().enumerate() {
924 futures::executor::block_on(async {
925 controller
926 .check()
927 .await
928 .map_err(|s| OperationError::from_state(s, &controller))
929 })?;
930
931 if let Err(e) = trash::os_limited::purge_all([item]) {
932 errors.push(e);
933 }
934
935 controller.set_progress(i as f32 / count as f32);
936 }
937
938 if !errors.is_empty() {
940 log::warn!("Failed to purge {} items:", errors.len());
941 for e in &errors {
942 log::warn!(" - {e}");
943 }
944
945 return Err(OperationError::from_err(
947 format!(
948 "Failed to delete {} of {} items. Check log for details.",
949 errors.len(),
950 count
951 ),
952 &controller,
953 ));
954 }
955
956 Ok(())
957 })
958 .await
959 .map_err(wrap_compio_spawn_error)?
960 .map_err(|e| OperationError::from_err(e, &controller))?;
961 }
962 Ok(OperationSelection::default())
963 }
964 Self::Extract {
965 paths,
966 to,
967 password,
968 } => {
969 let controller_clone = controller.clone();
970 compio::runtime::spawn(async move {
971 let extracted = compio::runtime::spawn_blocking(move || {
972 let controller = controller_clone;
973 let total_paths = paths.len();
974 let mut op_sel = OperationSelection::default();
975 let mut written_files = Vec::new();
976 let mut target_dirs = std::collections::HashSet::new();
977 for (i, path) in paths.iter().enumerate() {
978 futures::executor::block_on(async {
979 controller
980 .check()
981 .await
982 .map_err(|s| OperationError::from_state(s, &controller))
983 })?;
984
985 controller.set_progress((i as f32) / total_paths as f32);
986
987 if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
988 let dir_name = get_directory_name(file_name);
989 let mut new_dir = to.join(dir_name);
990
991 if new_dir.exists()
992 && let Some(new_dir_parent) = new_dir.parent()
993 {
994 new_dir = copy_unique_path(&new_dir, new_dir_parent);
995 }
996
997 op_sel.ignored.push(path.clone());
998 op_sel.selected.push(new_dir.clone());
999
1000 let (files, dirs) = crate::archive::extract(
1001 path,
1002 &new_dir,
1003 &password,
1004 &controller,
1005 )?;
1006 written_files.extend(files);
1007 target_dirs.extend(dirs);
1008 }
1009 }
1010
1011 Ok::<_, OperationError>((op_sel, written_files, target_dirs))
1012 })
1013 .await
1014 .map_err(wrap_compio_spawn_error)??;
1015
1016 let (op_sel, written_files, target_dirs) = extracted;
1017 if !written_files.is_empty() || !target_dirs.is_empty() {
1018 sync_to_disk(written_files, target_dirs).await;
1019 }
1020
1021 Ok::<_, OperationError>(op_sel)
1022 })
1023 .await
1024 .map_err(wrap_compio_spawn_error)?
1025 }
1026 Self::Move {
1027 paths,
1028 to,
1029 cross_device_copy,
1030 } => {
1031 copy_or_move(
1032 paths,
1033 to,
1034 Method::Move { cross_device_copy },
1035 msg_tx,
1036 controller,
1037 )
1038 .await
1039 }
1040 Self::NewFolder { path } => {
1041 let controller_clone = controller.clone();
1042 compio::runtime::spawn(async move {
1043 let controller = controller_clone;
1044 controller
1045 .check()
1046 .await
1047 .map_err(|s| OperationError::from_state(s, &controller))?;
1048 compio::fs::create_dir(&path)
1049 .await
1050 .map_err(|e| OperationError::from_err(e, &controller))?;
1051 Result::<_, OperationError>::Ok(OperationSelection {
1052 ignored: Vec::new(),
1053 selected: vec![path],
1054 })
1055 })
1056 }
1057 .await
1058 .map_err(wrap_compio_spawn_error)?,
1059 Self::NewFile { path } => {
1060 let controller_clone = controller.clone();
1061 compio::runtime::spawn(async move {
1062 let controller = controller_clone;
1063 controller
1064 .check()
1065 .await
1066 .map_err(|s| OperationError::from_state(s, &controller))?;
1067 compio::fs::File::create(&path)
1068 .await
1069 .map_err(|e| OperationError::from_err(e, &controller))?;
1070 Result::<_, OperationError>::Ok(OperationSelection {
1071 ignored: Vec::new(),
1072 selected: vec![path],
1073 })
1074 })
1075 }
1076 .await
1077 .map_err(wrap_compio_spawn_error)?,
1078 Self::PermanentlyDelete { paths } => {
1079 let total = paths.len();
1080 for (idx, path) in paths.into_iter().enumerate() {
1081 controller
1082 .check()
1083 .await
1084 .map_err(|s| OperationError::from_state(s, &controller))?;
1085
1086 controller.set_progress((idx as f32) / (total as f32));
1087
1088 tokio::task::spawn_blocking(|| {
1089 if path.is_symlink() || path.is_file() {
1090 fs::remove_file(path)
1091 } else if path.is_dir() {
1092 fs::remove_dir_all(path)
1093 } else {
1094 Err(std::io::Error::new(
1095 std::io::ErrorKind::InvalidData,
1096 "File to delete is not symlink, file or directory",
1097 ))
1098 }
1099 })
1100 .await
1101 .map_err(|e| OperationError::from_err(e, &controller))?
1102 .map_err(|e| OperationError::from_err(e, &controller))?;
1103 }
1104
1105 Ok(OperationSelection::default())
1106 }
1107 Self::RemoveFromRecents { paths } => {
1108 tokio::task::spawn_blocking(move || {
1109 let path_refs = paths.iter().map(PathBuf::as_path).collect::<Box<[_]>>();
1110 recently_used_xbel::remove_recently_used(&path_refs)
1111 })
1112 .await
1113 .map_err(|e| OperationError::from_err(e, &controller))?
1114 .map_err(|e| OperationError::from_err(e, &controller))?;
1115
1116 Ok(OperationSelection::default())
1117 }
1118 Self::Rename { from, to } => {
1119 let controller_clone = controller.clone();
1120
1121 compio::runtime::spawn(async move {
1122 let controller = controller_clone;
1123 controller
1124 .check()
1125 .await
1126 .map_err(|s| OperationError::from_state(s, &controller))?;
1127 compio::fs::rename(&from, &to)
1128 .await
1129 .map_err(|e| OperationError::from_err(e, &controller))?;
1130 Result::<_, OperationError>::Ok(OperationSelection {
1131 ignored: vec![from],
1132 selected: vec![to],
1133 })
1134 })
1135 }
1136 .await
1137 .map_err(wrap_compio_spawn_error)?,
1138 #[cfg(target_os = "macos")]
1139 Self::Restore { .. } => {
1140 return Err(OperationError::from_msg(
1142 "Restoring from trash is not supported on macos",
1143 ));
1144 }
1145 #[cfg(not(target_os = "macos"))]
1146 Self::Restore { items } => {
1147 let total = items.len();
1148 let mut paths = Vec::with_capacity(total);
1149 for (i, item) in items.into_iter().enumerate() {
1150 controller
1151 .check()
1152 .await
1153 .map_err(|s| OperationError::from_state(s, &controller))?;
1154
1155 controller.set_progress((i as f32) / (total as f32));
1156
1157 paths.push(item.original_path());
1158
1159 compio::runtime::spawn_blocking(|| trash::os_limited::restore_all([item]))
1160 .await
1161 .map_err(wrap_compio_spawn_error)?
1162 .map_err(|e| OperationError::from_err(e, &controller))?;
1163 }
1164 Ok(OperationSelection {
1165 ignored: Vec::new(),
1166 selected: paths,
1167 })
1168 }
1169 Self::SetExecutableAndLaunch { path } => {
1170 controller
1171 .check()
1172 .await
1173 .map_err(|s| OperationError::from_state(s, &controller))?;
1174
1175 let controller_clone = controller.clone();
1176 compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {
1177 let controller = controller_clone;
1178 #[cfg(unix)]
1180 {
1181 use std::os::unix::fs::PermissionsExt;
1182
1183 let mut perms = fs::metadata(&path)
1184 .map_err(|e| OperationError::from_err(e, &controller))?
1185 .permissions();
1186 let current_mode = perms.mode();
1187 let new_mode = current_mode | 0o111;
1188 perms.set_mode(new_mode);
1189 fs::set_permissions(&path, perms)
1190 .map_err(|e| OperationError::from_err(e, &controller))?;
1191 }
1192
1193 let mut command = std::process::Command::new(path);
1194 spawn_detached(&mut command)
1195 .map_err(|e| OperationError::from_err(e, &controller))?;
1196
1197 Ok(())
1198 })
1199 .await
1200 .map_err(wrap_compio_spawn_error)?
1201 .map_err(|e| OperationError::from_err(e, &controller))?;
1202 Ok(OperationSelection::default())
1203 }
1204 Self::SetPermissions { path, mode: _ } => {
1205 controller
1206 .check()
1207 .await
1208 .map_err(|s| OperationError::from_state(s, &controller))?;
1209
1210 let controller_clone = controller.clone();
1211 let path_clone = path.clone();
1212 compio::runtime::spawn_blocking(move || -> Result<(), OperationError> {
1213 let _controller = controller_clone;
1214 let _path = path_clone;
1215 #[cfg(unix)]
1217 {
1218 use std::os::unix::fs::PermissionsExt;
1219 let perms = fs::Permissions::from_mode(mode);
1220 fs::set_permissions(&path, perms)
1221 .map_err(|e| OperationError::from_err(e, &controller))?;
1222 }
1223
1224 Ok(())
1225 })
1226 .await
1227 .map_err(wrap_compio_spawn_error)?
1228 .map_err(|e| OperationError::from_err(e, &controller))?;
1229 Ok(OperationSelection {
1230 ignored: Vec::new(),
1231 selected: vec![path],
1232 })
1233 }
1234 };
1235
1236 controller_clone.set_progress(1.0);
1237
1238 paths
1239 }
1240}
1241
1242#[track_caller]
1243fn wrap_compio_spawn_error(err: Box<dyn std::any::Any + Send>) -> OperationError {
1244 log::error!(
1245 "compio runtime spawn failed: {}",
1246 std::backtrace::Backtrace::capture()
1247 );
1248
1249 if let Ok(err) = err.downcast() {
1251 *err
1252 } else {
1253 OperationError::from_msg("compio runtime spawn failed")
1254 }
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259 use std::fs::{self, File};
1260 use std::io;
1261 use std::path::PathBuf;
1262
1263 use cosmic::iced::futures::channel::mpsc;
1264 use cosmic::iced::futures::{StreamExt, future};
1265 use log::debug;
1266 use test_log::test;
1267 use tokio::sync;
1268
1269 use super::{Controller, Operation, OperationError, OperationSelection, ReplaceResult};
1270 use crate::app::test_utils::{
1271 NAME_LEN, NUM_DIRS, NUM_FILES, NUM_HIDDEN, NUM_NESTED, empty_fs, filter_dirs, filter_files,
1272 simple_fs,
1273 };
1274 use crate::app::{DialogPage, Message};
1275 use crate::fl;
1276
1277 pub async fn operation_copy(
1279 paths: Vec<PathBuf>,
1280 to: PathBuf,
1281 ) -> Result<OperationSelection, OperationError> {
1282 let id = fastrand::u64(0..u64::MAX);
1283 let (tx, mut rx) = mpsc::channel(1);
1284 let paths_clone = paths.clone();
1285 let to_clone = to.clone();
1286
1287 let handle_copy = async move {
1289 Operation::Copy {
1290 paths: paths_clone,
1291 to: to_clone,
1292 }
1293 .perform(&sync::Mutex::new(tx).into(), Controller::default())
1294 .await
1295 };
1296
1297 let handle_messages = async move {
1299 while let Some(msg) = rx.next().await {
1300 match msg {
1301 Message::DialogPush(DialogPage::Replace { tx, .. }, _id_to_focus) => {
1302 debug!("[{id}] Replace request");
1303 tx.send(ReplaceResult::Cancel)
1304 .await
1305 .expect("Sending a response to a replace request should succeed");
1306 }
1307 _ => unreachable!(
1308 "Only [ `Message::PendingProgress`, `Message::DialogPush(DialogPage::Replace)` ] are sent from operation"
1309 ),
1310 }
1311 }
1312 };
1313
1314 future::join(handle_messages, handle_copy).await.1
1315 }
1316
1317 #[test(compio::test)]
1318 async fn copy_file_to_same_location() -> io::Result<()> {
1319 let fs = simple_fs(NUM_FILES, 0, 1, 0, NAME_LEN)?;
1320 let path = fs.path();
1321
1322 let first_dir = filter_dirs(path)?
1324 .next()
1325 .expect("Should have at least one directory");
1326 let first_file = filter_files(&first_dir)?
1327 .next()
1328 .expect("Should have at least one file");
1329
1330 let base_name = first_file
1332 .file_name()
1333 .and_then(|name| name.to_str())
1334 .expect("File name exists and is valid");
1335 debug!(
1336 "Duplicating {} in {}",
1337 first_file.display(),
1338 first_dir.display()
1339 );
1340 operation_copy(vec![first_file.clone()], first_dir.clone())
1341 .await
1342 .expect("Copy operation should have succeeded");
1343
1344 assert!(first_file.exists(), "Original file should still exist");
1345 let expected = first_dir.join(format!("{base_name} ({} 1)", fl!("copy_noun")));
1346 assert!(expected.exists(), "File should have been duplicated");
1347
1348 Ok(())
1349 }
1350
1351 #[test(compio::test)]
1352 async fn copy_file_with_extension_to_same_loc() -> io::Result<()> {
1353 let fs = empty_fs()?;
1354 let path = fs.path();
1355
1356 let base_name = "foo.txt";
1357 let base_path = path.join(base_name);
1358 File::create(&base_path)?;
1359 debug!("Duplicating {}", base_path.display());
1360 operation_copy(vec![base_path.clone()], path.to_owned())
1361 .await
1362 .expect("Copy operation should have succeeded");
1363
1364 assert!(base_path.exists(), "Original file should still exist");
1365 let expected = path.join(format!("foo ({} 1).txt", fl!("copy_noun")));
1366 assert!(expected.exists(), "File should have been duplicated");
1367
1368 Ok(())
1369 }
1370
1371 #[test(compio::test)]
1372 async fn copy_dir_to_same_location() -> io::Result<()> {
1373 let fs = simple_fs(NUM_FILES, 0, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
1374 let path = fs.path();
1375
1376 let first_dir = filter_dirs(path)?
1378 .next()
1379 .expect("Should have at least one directory");
1380 let base_name = first_dir
1381 .file_name()
1382 .and_then(|name| name.to_str())
1383 .expect("First directory exists and has a valid name");
1384 debug!("Duplicating directory {}", first_dir.display());
1385 operation_copy(vec![first_dir.clone()], path.to_owned())
1386 .await
1387 .expect("Copy operation should have succeeded");
1388
1389 assert!(first_dir.exists(), "Original directory should still exist");
1390 let expected = path.join(format!("{base_name} ({} 1)", fl!("copy_noun")));
1391 assert!(expected.exists(), "Directory should have been duplicated");
1392
1393 Ok(())
1394 }
1395
1396 #[test(compio::test)]
1397 async fn copying_file_multiple_times_to_same_location() -> io::Result<()> {
1398 let fs = empty_fs()?;
1399 let path = fs.path();
1400
1401 let base_name = "cosmic";
1402 let base_path = path.join(base_name);
1403 File::create(&base_path)?;
1404
1405 for i in 1..5 {
1406 debug!("Duplicating {}", base_path.display());
1407 operation_copy(vec![base_path.clone()], path.to_owned())
1408 .await
1409 .expect("Copy operation should have succeeded");
1410 assert!(base_path.exists(), "Original file should still exist");
1411 assert!(
1412 path.join(format!("{base_name} ({} {i})", fl!("copy_noun")))
1413 .exists(),
1414 "File should have been duplicated (copy #{i})"
1415 );
1416 }
1417
1418 Ok(())
1419 }
1420
1421 #[test(compio::test)]
1422 async fn copy_to_diff_dir_doesnt_dupe_files() -> io::Result<()> {
1423 let fs = simple_fs(NUM_FILES, NUM_HIDDEN, NUM_DIRS, NUM_NESTED, NAME_LEN)?;
1424 let path = fs.path();
1425
1426 let (first_dir, second_dir) = {
1427 let mut dirs = filter_dirs(path)?;
1428 (
1429 dirs.next().expect("Should have at least two dirs"),
1430 dirs.next().expect("Should have at least two dirs"),
1431 )
1432 };
1433 let first_file = filter_files(&first_dir)?
1434 .next()
1435 .expect("Should have at least one file");
1436 let base_name = first_file
1438 .file_name()
1439 .and_then(|name| name.to_str())
1440 .expect("File name exists and is valid");
1441
1442 debug!(
1443 "Copying {} to {}",
1444 first_file.display(),
1445 second_dir.display()
1446 );
1447 operation_copy(vec![first_file.clone()], second_dir.clone())
1448 .await
1449 .expect(concat!(
1450 "Copy operation should have been cancelled ",
1451 "because we're copying to different directories ",
1452 "without replacement"
1453 ));
1454 assert!(
1455 first_dir.join(base_name).exists(),
1456 "First file should still exist"
1457 );
1458 assert!(
1459 second_dir.join(base_name).exists(),
1460 "Second file should still exist"
1461 );
1462
1463 Ok(())
1464 }
1465
1466 #[test(compio::test)]
1467 async fn copy_file_with_diff_name_to_diff_dir() -> io::Result<()> {
1468 let fs = empty_fs()?;
1469 let path = fs.path();
1470
1471 let dir_path = path.join("cosmic");
1472 fs::create_dir(&dir_path)?;
1473 let file_path = path.join("ferris");
1474 File::create(&file_path)?;
1475 let expected = dir_path.join("ferris");
1476
1477 debug!("Copying {} to {}", file_path.display(), expected.display());
1478 operation_copy(vec![file_path.clone()], dir_path.clone())
1479 .await
1480 .expect("Copy operation should have succeeded");
1481
1482 assert!(file_path.exists(), "Original file should still exist");
1483 assert!(expected.exists(), "File should have been copied");
1484
1485 Ok(())
1486 }
1487}