gravityfile_ops/
executor.rs

1//! High-level operation executor with unified result handling.
2
3use std::path::PathBuf;
4
5use tokio::sync::mpsc;
6
7use crate::conflict::ConflictResolution;
8use crate::copy::{start_copy, CopyOptions, CopyResult};
9use crate::create::{start_create_directory, start_create_file, CreateResult};
10use crate::move_op::{start_move, MoveOptions, MoveResult};
11use crate::progress::{OperationComplete, OperationProgress};
12use crate::rename::{start_rename, RenameResult};
13use crate::undo::UndoableOperation;
14use crate::{Conflict, OPERATION_CHANNEL_SIZE};
15
16/// Unified result type for all operations.
17#[derive(Debug)]
18pub enum OperationResult {
19    /// Progress update.
20    Progress(OperationProgress),
21    /// A conflict needs resolution.
22    Conflict(Conflict),
23    /// The operation completed.
24    Complete(OperationComplete),
25}
26
27/// Executor for file operations with unified interface.
28#[derive(Debug, Default)]
29pub struct OperationExecutor {
30    /// Default conflict resolution.
31    pub default_resolution: Option<ConflictResolution>,
32    /// Whether to use trash for deletions.
33    pub use_trash: bool,
34}
35
36impl OperationExecutor {
37    /// Create a new executor with default settings.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Create an executor that uses trash for deletions.
43    pub fn with_trash() -> Self {
44        Self {
45            use_trash: true,
46            ..Default::default()
47        }
48    }
49
50    /// Set the default conflict resolution.
51    pub fn with_resolution(mut self, resolution: ConflictResolution) -> Self {
52        self.default_resolution = Some(resolution);
53        self
54    }
55
56    /// Execute a copy operation.
57    pub fn copy(
58        &self,
59        sources: Vec<PathBuf>,
60        destination: PathBuf,
61    ) -> mpsc::Receiver<OperationResult> {
62        let options = CopyOptions {
63            conflict_resolution: self.default_resolution,
64            preserve_timestamps: true,
65        };
66
67        let copy_rx = start_copy(sources, destination, options);
68        Self::adapt_copy_results(copy_rx)
69    }
70
71    /// Execute a move operation.
72    pub fn move_to(
73        &self,
74        sources: Vec<PathBuf>,
75        destination: PathBuf,
76    ) -> mpsc::Receiver<OperationResult> {
77        let options = MoveOptions {
78            conflict_resolution: self.default_resolution,
79        };
80
81        let move_rx = start_move(sources, destination, options);
82        Self::adapt_move_results(move_rx)
83    }
84
85    /// Execute a rename operation.
86    pub fn rename(&self, source: PathBuf, new_name: String) -> mpsc::Receiver<OperationResult> {
87        let rename_rx = start_rename(source, new_name);
88        Self::adapt_rename_results(rename_rx)
89    }
90
91    /// Execute a file creation operation.
92    pub fn create_file(&self, path: PathBuf) -> mpsc::Receiver<OperationResult> {
93        let create_rx = start_create_file(path);
94        Self::adapt_create_results(create_rx)
95    }
96
97    /// Execute a directory creation operation.
98    pub fn create_directory(&self, path: PathBuf) -> mpsc::Receiver<OperationResult> {
99        let create_rx = start_create_directory(path);
100        Self::adapt_create_results(create_rx)
101    }
102
103    /// Adapt copy results to unified result type.
104    fn adapt_copy_results(mut rx: mpsc::Receiver<CopyResult>) -> mpsc::Receiver<OperationResult> {
105        let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
106
107        tokio::spawn(async move {
108            while let Some(result) = rx.recv().await {
109                let unified = match result {
110                    CopyResult::Progress(p) => OperationResult::Progress(p),
111                    CopyResult::Conflict(c) => OperationResult::Conflict(c),
112                    CopyResult::Complete(c) => OperationResult::Complete(c),
113                };
114                if tx.send(unified).await.is_err() {
115                    break;
116                }
117            }
118        });
119
120        result_rx
121    }
122
123    /// Adapt move results to unified result type.
124    fn adapt_move_results(mut rx: mpsc::Receiver<MoveResult>) -> mpsc::Receiver<OperationResult> {
125        let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
126
127        tokio::spawn(async move {
128            while let Some(result) = rx.recv().await {
129                let unified = match result {
130                    MoveResult::Progress(p) => OperationResult::Progress(p),
131                    MoveResult::Conflict(c) => OperationResult::Conflict(c),
132                    MoveResult::Complete(c) => OperationResult::Complete(c),
133                };
134                if tx.send(unified).await.is_err() {
135                    break;
136                }
137            }
138        });
139
140        result_rx
141    }
142
143    /// Adapt rename results to unified result type.
144    fn adapt_rename_results(
145        mut rx: mpsc::Receiver<RenameResult>,
146    ) -> mpsc::Receiver<OperationResult> {
147        let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
148
149        tokio::spawn(async move {
150            while let Some(result) = rx.recv().await {
151                let unified = match result {
152                    RenameResult::Progress(p) => OperationResult::Progress(p),
153                    RenameResult::Complete(c) => OperationResult::Complete(c),
154                };
155                if tx.send(unified).await.is_err() {
156                    break;
157                }
158            }
159        });
160
161        result_rx
162    }
163
164    /// Adapt create results to unified result type.
165    fn adapt_create_results(
166        mut rx: mpsc::Receiver<CreateResult>,
167    ) -> mpsc::Receiver<OperationResult> {
168        let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
169
170        tokio::spawn(async move {
171            while let Some(result) = rx.recv().await {
172                let unified = match result {
173                    CreateResult::Progress(p) => OperationResult::Progress(p),
174                    CreateResult::Complete(c) => OperationResult::Complete(c),
175                };
176                if tx.send(unified).await.is_err() {
177                    break;
178                }
179            }
180        });
181
182        result_rx
183    }
184}
185
186/// Execute an undo operation.
187///
188/// Returns a receiver for the undo operation's progress.
189pub fn execute_undo(entry: crate::UndoEntry) -> mpsc::Receiver<OperationResult> {
190    let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
191
192    tokio::spawn(async move {
193        match entry.operation {
194            UndoableOperation::FilesMoved { moves } => {
195                // Reverse the moves
196                let reverse_moves: Vec<(PathBuf, PathBuf)> =
197                    moves.into_iter().map(|(from, to)| (to, from)).collect();
198
199                let options = MoveOptions {
200                    conflict_resolution: Some(ConflictResolution::Overwrite),
201                };
202
203                let sources: Vec<PathBuf> = reverse_moves.iter().map(|(from, _)| from.clone()).collect();
204                let mut move_rx = start_move(
205                    sources,
206                    reverse_moves
207                        .first()
208                        .and_then(|(_, to)| to.parent())
209                        .map(|p| p.to_path_buf())
210                        .unwrap_or_default(),
211                    options,
212                );
213
214                while let Some(result) = move_rx.recv().await {
215                    let unified = match result {
216                        MoveResult::Progress(p) => OperationResult::Progress(p),
217                        MoveResult::Conflict(c) => OperationResult::Conflict(c),
218                        MoveResult::Complete(c) => OperationResult::Complete(c),
219                    };
220                    if tx.send(unified).await.is_err() {
221                        break;
222                    }
223                }
224            }
225            UndoableOperation::FilesCopied { created } => {
226                // Delete the copied files
227                use crate::progress::OperationType;
228                use std::fs;
229
230                let mut progress = OperationProgress::new(OperationType::Delete, created.len(), 0);
231                let mut succeeded = 0;
232                let mut failed = 0;
233
234                for path in created {
235                    progress.set_current_file(Some(path.clone()));
236                    let _ = tx.send(OperationResult::Progress(progress.clone())).await;
237
238                    let result = tokio::task::spawn_blocking(move || {
239                        // Use symlink_metadata to check type without following symlinks
240                        let metadata = fs::symlink_metadata(&path)?;
241                        if metadata.is_symlink() {
242                            // Symlinks are always removed as files
243                            fs::remove_file(&path)
244                        } else if metadata.is_dir() {
245                            fs::remove_dir_all(&path)
246                        } else {
247                            fs::remove_file(&path)
248                        }
249                    })
250                    .await;
251
252                    match result {
253                        Ok(Ok(())) => {
254                            succeeded += 1;
255                            progress.complete_file(0);
256                        }
257                        _ => {
258                            failed += 1;
259                        }
260                    }
261                }
262
263                let _ = tx
264                    .send(OperationResult::Complete(OperationComplete {
265                        operation_type: OperationType::Delete,
266                        succeeded,
267                        failed,
268                        bytes_processed: 0,
269                        errors: progress.errors,
270                    }))
271                    .await;
272            }
273            UndoableOperation::FilesDeleted { trash_entries } => {
274                // Restore from trash (if possible)
275                use crate::progress::OperationType;
276
277                if trash_entries.is_empty() {
278                    let _ = tx
279                        .send(OperationResult::Complete(OperationComplete {
280                            operation_type: OperationType::Move,
281                            succeeded: 0,
282                            failed: 0,
283                            bytes_processed: 0,
284                            errors: vec![crate::OperationError::new(
285                                PathBuf::new(),
286                                "Cannot undo permanent deletion".to_string(),
287                            )],
288                        }))
289                        .await;
290                    return;
291                }
292
293                // Move files back from trash
294                let mut progress = OperationProgress::new(OperationType::Move, trash_entries.len(), 0);
295                let mut succeeded = 0;
296                let mut failed = 0;
297
298                for (original, trash) in trash_entries {
299                    progress.set_current_file(Some(trash.clone()));
300                    let _ = tx.send(OperationResult::Progress(progress.clone())).await;
301
302                    let result = tokio::task::spawn_blocking(move || std::fs::rename(&trash, &original))
303                        .await;
304
305                    match result {
306                        Ok(Ok(())) => {
307                            succeeded += 1;
308                            progress.complete_file(0);
309                        }
310                        _ => {
311                            failed += 1;
312                        }
313                    }
314                }
315
316                let _ = tx
317                    .send(OperationResult::Complete(OperationComplete {
318                        operation_type: OperationType::Move,
319                        succeeded,
320                        failed,
321                        bytes_processed: 0,
322                        errors: progress.errors,
323                    }))
324                    .await;
325            }
326            UndoableOperation::FileRenamed {
327                path,
328                old_name,
329                new_name: _,
330            } => {
331                // Rename back to old name
332                let mut rename_rx = start_rename(path, old_name);
333
334                while let Some(result) = rename_rx.recv().await {
335                    let unified = match result {
336                        RenameResult::Progress(p) => OperationResult::Progress(p),
337                        RenameResult::Complete(c) => OperationResult::Complete(c),
338                    };
339                    if tx.send(unified).await.is_err() {
340                        break;
341                    }
342                }
343            }
344            UndoableOperation::FileCreated { path } | UndoableOperation::DirectoryCreated { path } => {
345                // Delete the created item
346                use crate::progress::OperationType;
347                use std::fs;
348
349                let mut progress = OperationProgress::new(OperationType::Delete, 1, 0);
350                progress.set_current_file(Some(path.clone()));
351                let _ = tx.send(OperationResult::Progress(progress.clone())).await;
352
353                let path_clone = path.clone();
354                let result = tokio::task::spawn_blocking(move || {
355                    // Use symlink_metadata to check type without following symlinks
356                    let metadata = fs::symlink_metadata(&path_clone)?;
357                    if metadata.is_symlink() {
358                        // Symlinks are always removed as files
359                        fs::remove_file(&path_clone)
360                    } else if metadata.is_dir() {
361                        fs::remove_dir_all(&path_clone)
362                    } else {
363                        fs::remove_file(&path_clone)
364                    }
365                })
366                .await;
367
368                let (succeeded, failed) = match result {
369                    Ok(Ok(())) => (1, 0),
370                    _ => (0, 1),
371                };
372
373                let _ = tx
374                    .send(OperationResult::Complete(OperationComplete {
375                        operation_type: OperationType::Delete,
376                        succeeded,
377                        failed,
378                        bytes_processed: 0,
379                        errors: progress.errors,
380                    }))
381                    .await;
382            }
383        }
384    });
385
386    rx
387}