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                        if path.is_dir() {
240                            fs::remove_dir_all(&path)
241                        } else {
242                            fs::remove_file(&path)
243                        }
244                    })
245                    .await;
246
247                    match result {
248                        Ok(Ok(())) => {
249                            succeeded += 1;
250                            progress.complete_file(0);
251                        }
252                        _ => {
253                            failed += 1;
254                        }
255                    }
256                }
257
258                let _ = tx
259                    .send(OperationResult::Complete(OperationComplete {
260                        operation_type: OperationType::Delete,
261                        succeeded,
262                        failed,
263                        bytes_processed: 0,
264                        errors: progress.errors,
265                    }))
266                    .await;
267            }
268            UndoableOperation::FilesDeleted { trash_entries } => {
269                // Restore from trash (if possible)
270                use crate::progress::OperationType;
271
272                if trash_entries.is_empty() {
273                    let _ = tx
274                        .send(OperationResult::Complete(OperationComplete {
275                            operation_type: OperationType::Move,
276                            succeeded: 0,
277                            failed: 0,
278                            bytes_processed: 0,
279                            errors: vec![crate::OperationError::new(
280                                PathBuf::new(),
281                                "Cannot undo permanent deletion".to_string(),
282                            )],
283                        }))
284                        .await;
285                    return;
286                }
287
288                // Move files back from trash
289                let mut progress = OperationProgress::new(OperationType::Move, trash_entries.len(), 0);
290                let mut succeeded = 0;
291                let mut failed = 0;
292
293                for (original, trash) in trash_entries {
294                    progress.set_current_file(Some(trash.clone()));
295                    let _ = tx.send(OperationResult::Progress(progress.clone())).await;
296
297                    let result = tokio::task::spawn_blocking(move || std::fs::rename(&trash, &original))
298                        .await;
299
300                    match result {
301                        Ok(Ok(())) => {
302                            succeeded += 1;
303                            progress.complete_file(0);
304                        }
305                        _ => {
306                            failed += 1;
307                        }
308                    }
309                }
310
311                let _ = tx
312                    .send(OperationResult::Complete(OperationComplete {
313                        operation_type: OperationType::Move,
314                        succeeded,
315                        failed,
316                        bytes_processed: 0,
317                        errors: progress.errors,
318                    }))
319                    .await;
320            }
321            UndoableOperation::FileRenamed {
322                path,
323                old_name,
324                new_name: _,
325            } => {
326                // Rename back to old name
327                let mut rename_rx = start_rename(path, old_name);
328
329                while let Some(result) = rename_rx.recv().await {
330                    let unified = match result {
331                        RenameResult::Progress(p) => OperationResult::Progress(p),
332                        RenameResult::Complete(c) => OperationResult::Complete(c),
333                    };
334                    if tx.send(unified).await.is_err() {
335                        break;
336                    }
337                }
338            }
339            UndoableOperation::FileCreated { path } | UndoableOperation::DirectoryCreated { path } => {
340                // Delete the created item
341                use crate::progress::OperationType;
342                use std::fs;
343
344                let mut progress = OperationProgress::new(OperationType::Delete, 1, 0);
345                progress.set_current_file(Some(path.clone()));
346                let _ = tx.send(OperationResult::Progress(progress.clone())).await;
347
348                let path_clone = path.clone();
349                let result = tokio::task::spawn_blocking(move || {
350                    if path_clone.is_dir() {
351                        fs::remove_dir_all(&path_clone)
352                    } else {
353                        fs::remove_file(&path_clone)
354                    }
355                })
356                .await;
357
358                let (succeeded, failed) = match result {
359                    Ok(Ok(())) => (1, 0),
360                    _ => (0, 1),
361                };
362
363                let _ = tx
364                    .send(OperationResult::Complete(OperationComplete {
365                        operation_type: OperationType::Delete,
366                        succeeded,
367                        failed,
368                        bytes_processed: 0,
369                        errors: progress.errors,
370                    }))
371                    .await;
372            }
373        }
374    });
375
376    rx
377}