gravityfile_ops/
delete.rs1use std::path::PathBuf;
4
5use tokio::sync::mpsc;
6use tokio_util::sync::CancellationToken;
7
8use crate::progress::{OperationComplete, OperationProgress, OperationType, publish_progress};
9use crate::{OPERATION_CHANNEL_SIZE, OperationError};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum DeleteMode {
14 #[default]
16 Trash,
17 Permanent,
19}
20
21#[derive(Debug)]
23pub enum DeleteResult {
24 Progress(OperationProgress),
25 Complete(DeleteComplete),
26}
27
28#[derive(Debug)]
30pub struct DeleteComplete {
31 pub inner: OperationComplete,
32 pub deleted: Vec<PathBuf>,
34 pub mode: DeleteMode,
35}
36
37pub fn start_delete(
40 items: Vec<(PathBuf, u64)>,
41 mode: DeleteMode,
42 token: CancellationToken,
43) -> mpsc::Receiver<DeleteResult> {
44 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
45 tokio::spawn(async move {
46 let total_bytes = items
47 .iter()
48 .map(|(_, size)| *size)
49 .fold(0u64, u64::saturating_add);
50 let mut progress = OperationProgress::new(OperationType::Delete, items.len(), total_bytes);
51 let mut deleted = Vec::new();
52 let mut failed = 0;
53 for (path, size) in items {
54 if token.is_cancelled() || tx.is_closed() {
55 break;
56 }
57 progress.set_current_file(Some(path.clone()));
58 publish_progress(&tx, || DeleteResult::Progress(progress.clone()), &token);
59 let work_path = path.clone();
60 let result = tokio::task::spawn_blocking(move || delete_one(&work_path, mode))
61 .await
62 .unwrap_or_else(|error| Err(format!("worker failed: {error}")));
63 match result {
64 Ok(()) => {
65 progress.complete_file(size);
66 deleted.push(path);
67 }
68 Err(error) => {
69 progress.files_completed += 1;
70 progress.add_error(OperationError::new(path, error));
71 failed += 1;
72 }
73 }
74 }
75 let complete = DeleteComplete {
76 inner: OperationComplete {
77 cancelled: token.is_cancelled(),
78 operation_type: OperationType::Delete,
79 succeeded: deleted.len(),
80 failed,
81 skipped: 0,
82 bytes_processed: progress.bytes_processed,
83 errors: progress.errors,
84 },
85 deleted,
86 mode,
87 };
88 let _ = tx.send(DeleteResult::Complete(complete)).await;
89 });
90 rx
91}
92
93fn delete_one(path: &std::path::Path, mode: DeleteMode) -> Result<(), String> {
94 std::fs::symlink_metadata(path).map_err(|error| format!("Cannot access path: {error}"))?;
95 match mode {
96 DeleteMode::Trash => trash_context()
97 .delete(path)
98 .map_err(|error| format!("could not move to the Trash: {error}")),
99 DeleteMode::Permanent => crate::fs::remove_path(path).map_err(|error| error.to_string()),
100 }
101}
102
103pub(crate) fn trash_context() -> trash::TrashContext {
108 #[cfg_attr(not(target_os = "macos"), allow(unused_mut))]
109 let mut context = trash::TrashContext::default();
110 #[cfg(target_os = "macos")]
111 {
112 use trash::macos::{DeleteMethod, TrashContextExtMacos};
113 context.set_delete_method(DeleteMethod::NsFileManager);
114 }
115 context
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[tokio::test]
123 async fn permanent_delete_removes_links_not_targets() {
124 let dir = tempfile::tempdir().unwrap();
125 let tree = dir.path().join("tree");
126 std::fs::create_dir_all(tree.join("inner")).unwrap();
127 std::fs::write(tree.join("inner/file"), "x").unwrap();
128 let target = dir.path().join("target");
129 std::fs::write(&target, "keep").unwrap();
130 #[cfg_attr(not(unix), allow(unused_mut))]
131 let mut items = vec![(tree.clone(), 1), (dir.path().join("missing"), 0)];
132 #[cfg(unix)]
133 {
134 let link = dir.path().join("link");
135 std::os::unix::fs::symlink(&target, &link).unwrap();
136 items.push((link, 0));
137 }
138 let mut rx = start_delete(items, DeleteMode::Permanent, CancellationToken::new());
139 let complete = loop {
140 if let Some(DeleteResult::Complete(complete)) = rx.recv().await {
141 break complete;
142 }
143 };
144 assert_eq!(complete.inner.failed, 1);
145 assert!(!tree.exists());
146 assert_eq!(std::fs::read_to_string(target).unwrap(), "keep");
147 }
148}