1use std::fs;
4use std::path::PathBuf;
5
6use tokio::sync::mpsc;
7use tokio_util::sync::CancellationToken;
8
9use crate::conflict::ConflictResolution;
10use crate::copy::{CopyOptions, CopyResult, start_copy};
11use crate::create::{CreateResult, start_create_directory, start_create_file};
12use crate::move_op::{MoveOptions, MoveResult, start_move};
13use crate::progress::{OperationComplete, OperationProgress, OperationType};
14use crate::rename::{RenameResult, start_rename};
15use crate::undo::UndoableOperation;
16use crate::{Conflict, OPERATION_CHANNEL_SIZE};
17
18#[derive(Debug)]
20pub enum OperationResult {
21 Progress(OperationProgress),
23 Conflict(Conflict),
25 Complete(OperationComplete),
27}
28
29#[derive(Debug, Default)]
31pub struct OperationExecutor {
32 pub default_resolution: Option<ConflictResolution>,
34 pub use_trash: bool,
36}
37
38impl OperationExecutor {
39 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn with_trash() -> Self {
46 Self {
47 use_trash: true,
48 ..Default::default()
49 }
50 }
51
52 pub fn with_resolution(mut self, resolution: ConflictResolution) -> Self {
54 self.default_resolution = Some(resolution);
55 self
56 }
57
58 pub fn copy(
64 &self,
65 sources: Vec<PathBuf>,
66 destination: PathBuf,
67 ) -> mpsc::Receiver<OperationResult> {
68 self.copy_with_cancellation(sources, destination, CancellationToken::new())
69 }
70
71 pub fn copy_with_cancellation(
73 &self,
74 sources: Vec<PathBuf>,
75 destination: PathBuf,
76 token: CancellationToken,
77 ) -> mpsc::Receiver<OperationResult> {
78 let options = CopyOptions {
79 conflict_resolution: self.default_resolution,
80 preserve_timestamps: true,
81 };
82
83 let copy_rx = start_copy(sources, destination, options, token);
84 Self::adapt_copy_results(copy_rx)
85 }
86
87 pub fn move_to(
93 &self,
94 sources: Vec<PathBuf>,
95 destination: PathBuf,
96 ) -> mpsc::Receiver<OperationResult> {
97 self.move_to_with_cancellation(sources, destination, CancellationToken::new())
98 }
99
100 pub fn move_to_with_cancellation(
102 &self,
103 sources: Vec<PathBuf>,
104 destination: PathBuf,
105 token: CancellationToken,
106 ) -> mpsc::Receiver<OperationResult> {
107 let options = MoveOptions {
108 conflict_resolution: self.default_resolution,
109 };
110
111 let move_rx = start_move(sources, destination, options, token);
112 Self::adapt_move_results(move_rx)
113 }
114
115 pub fn rename(&self, source: PathBuf, new_name: String) -> mpsc::Receiver<OperationResult> {
117 let rename_rx = start_rename(source, new_name);
118 Self::adapt_rename_results(rename_rx)
119 }
120
121 pub fn create_file(&self, path: PathBuf) -> mpsc::Receiver<OperationResult> {
123 let create_rx = start_create_file(path);
124 Self::adapt_create_results(create_rx)
125 }
126
127 pub fn create_directory(&self, path: PathBuf) -> mpsc::Receiver<OperationResult> {
129 let create_rx = start_create_directory(path);
130 Self::adapt_create_results(create_rx)
131 }
132
133 fn adapt_copy_results(mut rx: mpsc::Receiver<CopyResult>) -> mpsc::Receiver<OperationResult> {
135 let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
136
137 tokio::spawn(async move {
138 while let Some(result) = rx.recv().await {
139 let unified = match result {
140 CopyResult::Progress(p) => OperationResult::Progress(p),
141 CopyResult::Conflict(c) => OperationResult::Conflict(c),
142 CopyResult::Complete(c) => OperationResult::Complete(c),
143 };
144 if tx.send(unified).await.is_err() {
145 break;
146 }
147 }
148 });
149
150 result_rx
151 }
152
153 fn adapt_move_results(mut rx: mpsc::Receiver<MoveResult>) -> mpsc::Receiver<OperationResult> {
159 let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
160
161 tokio::spawn(async move {
162 while let Some(result) = rx.recv().await {
163 let unified = match result {
164 MoveResult::Progress(p) => OperationResult::Progress(p),
165 MoveResult::Conflict(c) => OperationResult::Conflict(c),
166 MoveResult::Complete(mc) => OperationResult::Complete(mc.inner),
167 };
168 if tx.send(unified).await.is_err() {
169 break;
170 }
171 }
172 });
173
174 result_rx
175 }
176
177 fn adapt_rename_results(
179 mut rx: mpsc::Receiver<RenameResult>,
180 ) -> mpsc::Receiver<OperationResult> {
181 let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
182
183 tokio::spawn(async move {
184 while let Some(result) = rx.recv().await {
185 let unified = match result {
186 RenameResult::Progress(p) => OperationResult::Progress(p),
187 RenameResult::Complete(c) => OperationResult::Complete(c),
188 };
189 if tx.send(unified).await.is_err() {
190 break;
191 }
192 }
193 });
194
195 result_rx
196 }
197
198 fn adapt_create_results(
200 mut rx: mpsc::Receiver<CreateResult>,
201 ) -> mpsc::Receiver<OperationResult> {
202 let (tx, result_rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
203
204 tokio::spawn(async move {
205 while let Some(result) = rx.recv().await {
206 let unified = match result {
207 CreateResult::Progress(p) => OperationResult::Progress(p),
208 CreateResult::Complete(c) => OperationResult::Complete(c),
209 };
210 if tx.send(unified).await.is_err() {
211 break;
212 }
213 }
214 });
215
216 result_rx
217 }
218}
219
220pub fn execute_undo(entry: crate::UndoEntry) -> mpsc::Receiver<OperationResult> {
224 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
225
226 tokio::spawn(async move {
227 match entry.operation {
228 UndoableOperation::FilesMoved { moves } => {
229 let total = moves.len();
234 let mut progress = OperationProgress::new(OperationType::Move, total, 0);
235 let mut succeeded = 0usize;
236 let mut failed = 0usize;
237
238 for (original, current) in moves {
239 progress.set_current_file(Some(current.clone()));
241 let _ = tx.send(OperationResult::Progress(progress.clone())).await;
242
243 let orig_clone = original.clone();
244 let cur_clone = current.clone();
245
246 let result = tokio::task::spawn_blocking(move || {
247 if let Some(parent) = orig_clone.parent() {
249 fs::create_dir_all(parent)
250 .map_err(|e| format!("Failed to create parent directory: {}", e))?;
251 }
252 match fs::rename(&cur_clone, &orig_clone) {
256 Ok(()) => Ok(()),
257 Err(_rename_err) => {
258 let meta = fs::symlink_metadata(&cur_clone)
259 .map_err(|e| format!("Failed to read metadata: {}", e))?;
260 if meta.is_dir() {
261 let mut visited = std::collections::HashSet::new();
262 crate::move_op::copy_dir_recursive_pub(
263 &cur_clone,
264 &orig_clone,
265 &mut visited,
266 )?;
267 fs::remove_dir_all(&cur_clone)
268 .map_err(|e| format!("Failed to remove source: {}", e))
269 } else {
270 fs::copy(&cur_clone, &orig_clone)
271 .map_err(|e| format!("Failed to copy: {}", e))?;
272 fs::remove_file(&cur_clone)
273 .map_err(|e| format!("Failed to remove source: {}", e))
274 }
275 }
276 }
277 })
278 .await;
279
280 match result {
281 Ok(Ok(())) => {
282 progress.complete_file(0);
283 succeeded += 1;
284 }
285 Ok(Err(e)) => {
286 progress.add_error(crate::OperationError::new(current, e));
287 failed += 1;
288 }
289 Err(e) => {
290 progress.add_error(crate::OperationError::new(current, e.to_string()));
291 failed += 1;
292 }
293 }
294 }
295
296 let _ = tx
297 .send(OperationResult::Complete(OperationComplete {
298 operation_type: OperationType::Move,
299 succeeded,
300 failed,
301 bytes_processed: 0,
302 errors: progress.errors,
303 }))
304 .await;
305 }
306 UndoableOperation::FilesCopied { created } => {
307 let mut progress = OperationProgress::new(OperationType::Delete, created.len(), 0);
309 let mut succeeded = 0;
310 let mut failed = 0;
311
312 for path in created {
313 progress.set_current_file(Some(path.clone()));
314 let _ = tx.send(OperationResult::Progress(progress.clone())).await;
315
316 let result = tokio::task::spawn_blocking(move || {
317 let metadata = fs::symlink_metadata(&path)?;
319 if metadata.is_symlink() {
320 fs::remove_file(&path)
321 } else if metadata.is_dir() {
322 fs::remove_dir_all(&path)
323 } else {
324 fs::remove_file(&path)
325 }
326 })
327 .await;
328
329 match result {
330 Ok(Ok(())) => {
331 succeeded += 1;
332 progress.complete_file(0);
333 }
334 _ => {
335 failed += 1;
336 }
337 }
338 }
339
340 let _ = tx
341 .send(OperationResult::Complete(OperationComplete {
342 operation_type: OperationType::Delete,
343 succeeded,
344 failed,
345 bytes_processed: 0,
346 errors: progress.errors,
347 }))
348 .await;
349 }
350 UndoableOperation::FilesDeleted { paths: _ } => {
351 let _ = tx
352 .send(OperationResult::Complete(OperationComplete {
353 operation_type: OperationType::Delete,
354 succeeded: 0,
355 failed: 0,
356 bytes_processed: 0,
357 errors: vec![crate::OperationError::new(
358 PathBuf::new(),
359 "Cannot undo trash operations from within the application".to_string(),
360 )],
361 }))
362 .await;
363 }
364 UndoableOperation::FileRenamed {
365 path: current_path,
366 old_path,
367 new_name: _,
368 } => {
369 let old_name = old_path
373 .file_name()
374 .map(|n| n.to_string_lossy().into_owned())
375 .unwrap_or_default();
376
377 let mut rename_rx = start_rename(current_path, old_name);
378
379 while let Some(result) = rename_rx.recv().await {
380 let unified = match result {
381 RenameResult::Progress(p) => OperationResult::Progress(p),
382 RenameResult::Complete(c) => OperationResult::Complete(c),
383 };
384 if tx.send(unified).await.is_err() {
385 break;
386 }
387 }
388 }
389 UndoableOperation::FileCreated { path }
390 | UndoableOperation::DirectoryCreated { path } => {
391 let mut progress = OperationProgress::new(OperationType::Delete, 1, 0);
393 progress.set_current_file(Some(path.clone()));
394 let _ = tx.send(OperationResult::Progress(progress.clone())).await;
395
396 let path_clone = path.clone();
397 let result = tokio::task::spawn_blocking(move || {
398 let metadata = fs::symlink_metadata(&path_clone)?;
399 if metadata.is_symlink() {
400 fs::remove_file(&path_clone)
401 } else if metadata.is_dir() {
402 fs::remove_dir_all(&path_clone)
403 } else {
404 fs::remove_file(&path_clone)
405 }
406 })
407 .await;
408
409 let (succeeded, failed) = match result {
410 Ok(Ok(())) => (1, 0),
411 _ => (0, 1),
412 };
413
414 let _ = tx
415 .send(OperationResult::Complete(OperationComplete {
416 operation_type: OperationType::Delete,
417 succeeded,
418 failed,
419 bytes_processed: 0,
420 errors: progress.errors,
421 }))
422 .await;
423 }
424 }
425 });
426
427 rx
428}