Skip to main content

gravityfile_ops/
delete.rs

1//! Deletion to the system Trash or permanently.
2
3use 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/// Where deleted items go.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum DeleteMode {
14    /// The system Trash / Recycle Bin (recoverable).
15    #[default]
16    Trash,
17    /// Unlinked immediately (not recoverable).
18    Permanent,
19}
20
21/// Message sent while a delete runs.
22#[derive(Debug)]
23pub enum DeleteResult {
24    Progress(OperationProgress),
25    Complete(DeleteComplete),
26}
27
28/// Completion of a delete.
29#[derive(Debug)]
30pub struct DeleteComplete {
31    pub inner: OperationComplete,
32    /// Items that were removed, in request order.
33    pub deleted: Vec<PathBuf>,
34    pub mode: DeleteMode,
35}
36
37/// Delete `items`, each given as `(path, size)`; the size feeds progress.
38/// Symlinks are removed themselves, never their targets.
39pub 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
103/// The Trash context used for every trash operation in this crate.
104///
105/// On macOS the Finder (AppleScript) method needs automation permission and
106/// spawns a process per call; `NSFileManager` is fast and prompt-free.
107pub(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}