1use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Duration;
8
9use tokio::sync::mpsc;
10use tokio_util::sync::CancellationToken;
11
12use crate::conflict::{
13 Conflict, ConflictKind, ConflictRequest, ConflictResolution, auto_rename_path,
14 request_resolution,
15};
16use crate::fs::{RenameError, is_within_directory, remove_path, rename_noreplace, same_object};
17use crate::progress::{OperationComplete, OperationProgress, OperationType, publish_progress};
18use crate::transfer::{Preserve, ReplacePolicy, Transfer, dispose};
19use crate::{OPERATION_CHANNEL_SIZE, OperationError};
20
21const PROGRESS_INTERVAL: Duration = Duration::from_millis(100);
23
24#[derive(Debug, Clone, Copy)]
26pub struct TransferOptions {
27 pub conflict_resolution: Option<ConflictResolution>,
29 pub replace_policy: ReplacePolicy,
31 pub preserve_timestamps: bool,
33 pub preserve_xattrs: bool,
35}
36
37impl Default for TransferOptions {
38 fn default() -> Self {
39 Self {
40 conflict_resolution: None,
41 replace_policy: ReplacePolicy::Trash,
42 preserve_timestamps: true,
43 preserve_xattrs: true,
44 }
45 }
46}
47
48#[derive(Debug)]
50pub enum TransferResult {
51 Progress(OperationProgress),
53 Conflict(ConflictRequest),
55 Complete(TransferComplete),
57}
58
59#[derive(Debug)]
61pub struct TransferComplete {
62 pub inner: OperationComplete,
63 pub completed: Vec<(PathBuf, PathBuf)>,
67 pub replaced: Vec<(PathBuf, PathBuf)>,
69}
70
71impl TransferComplete {
72 pub fn touched_paths(&self, moved: bool) -> Vec<PathBuf> {
75 let mut touched = Vec::new();
76 for (source, destination) in self.completed.iter().chain(&self.replaced) {
77 if moved {
78 touched.push(source.clone());
79 }
80 touched.push(destination.clone());
81 }
82 touched
83 }
84 pub fn created_paths(&self) -> impl Iterator<Item = &Path> {
86 self.completed
87 .iter()
88 .map(|(_, destination)| destination.as_path())
89 }
90}
91
92pub fn start_copy(
94 sources: Vec<PathBuf>,
95 destination: PathBuf,
96 options: TransferOptions,
97 token: CancellationToken,
98) -> mpsc::Receiver<TransferResult> {
99 start(Kind::Copy, sources, destination, options, token)
100}
101
102pub fn start_move(
106 sources: Vec<PathBuf>,
107 destination: PathBuf,
108 options: TransferOptions,
109 token: CancellationToken,
110) -> mpsc::Receiver<TransferResult> {
111 start(Kind::Move, sources, destination, options, token)
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115enum Kind {
116 Copy,
117 Move,
118}
119
120impl Kind {
121 fn operation_type(self) -> OperationType {
122 match self {
123 Self::Copy => OperationType::Copy,
124 Self::Move => OperationType::Move,
125 }
126 }
127}
128
129fn start(
130 kind: Kind,
131 sources: Vec<PathBuf>,
132 destination: PathBuf,
133 options: TransferOptions,
134 token: CancellationToken,
135) -> mpsc::Receiver<TransferResult> {
136 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
137 tokio::spawn(async move {
138 let receiver = tx.clone();
139 let work = Batch::new(kind, options, token.clone(), tx).run(sources, destination);
140 tokio::pin!(work);
141 tokio::select! {
142 biased;
143 _ = receiver.closed() => {
144 token.cancel();
145 work.await;
147 }
148 _ = &mut work => {}
149 }
150 });
151 rx
152}
153
154struct Batch {
155 kind: Kind,
156 options: TransferOptions,
157 token: CancellationToken,
158 tx: mpsc::Sender<TransferResult>,
159 progress: OperationProgress,
160 global: Option<ConflictResolution>,
161 succeeded: usize,
162 failed: usize,
163 skipped: usize,
164 completed: Vec<(PathBuf, PathBuf)>,
165 replaced: Vec<(PathBuf, PathBuf)>,
166}
167
168enum Plan {
169 Skip,
170 Abort,
171 Publish { destination: PathBuf, replace: bool },
172}
173
174impl Batch {
175 fn new(
176 kind: Kind,
177 options: TransferOptions,
178 token: CancellationToken,
179 tx: mpsc::Sender<TransferResult>,
180 ) -> Self {
181 Self {
182 kind,
183 global: options.conflict_resolution,
184 options,
185 token,
186 tx,
187 progress: OperationProgress::new(kind.operation_type(), 0, 0),
188 succeeded: 0,
189 failed: 0,
190 skipped: 0,
191 completed: Vec::new(),
192 replaced: Vec::new(),
193 }
194 }
195
196 async fn run(mut self, sources: Vec<PathBuf>, destination: PathBuf) {
197 if let Err(error) = self.prepare(&sources, &destination).await {
198 self.progress.add_error(error);
199 self.failed = sources.len();
200 return self.finish(false).await;
201 }
202 for source in sources {
203 if self.token.is_cancelled() {
204 break;
205 }
206 match self.plan(&source, &destination).await {
207 Plan::Skip => self.skipped += 1,
208 Plan::Abort => return self.finish(true).await,
209 Plan::Publish {
210 destination,
211 replace,
212 } => self.publish(source, destination, replace).await,
213 }
214 }
215 self.finish(false).await
216 }
217
218 async fn prepare(
220 &mut self,
221 sources: &[PathBuf],
222 destination: &Path,
223 ) -> Result<(), OperationError> {
224 if let Some(source) = sources
225 .iter()
226 .find(|source| is_within_directory(source, destination))
227 {
228 return Err(OperationError::new(
229 source.clone(),
230 format!(
231 "cannot {} a directory into itself ('{}' is inside it)",
232 if self.kind == Kind::Copy {
233 "copy"
234 } else {
235 "move"
236 },
237 destination.display()
238 ),
239 ));
240 }
241 fs::create_dir_all(destination).map_err(|error| {
242 OperationError::new(
243 destination.to_path_buf(),
244 format!("cannot create destination: {error}"),
245 )
246 })?;
247 if !fs::metadata(destination).is_ok_and(|metadata| metadata.is_dir()) {
248 return Err(OperationError::new(
249 destination.to_path_buf(),
250 "destination is not a directory",
251 ));
252 }
253 self.progress.files_total = sources.len();
254 if self.kind == Kind::Copy {
257 let sources = sources.to_vec();
258 let token = self.token.clone();
259 self.progress.bytes_total =
260 tokio::task::spawn_blocking(move || total_bytes(&sources, &token))
261 .await
262 .unwrap_or(0);
263 }
264 Ok(())
265 }
266
267 async fn plan(&mut self, source: &Path, destination_dir: &Path) -> Plan {
268 let Some(name) = source.file_name() else {
269 self.fail(source, "path has no file name");
270 return Plan::Skip;
271 };
272 let destination = destination_dir.join(name);
273 if source == destination || same_object(source, &destination) {
274 return match self
275 .ask(Conflict::new(
276 source.into(),
277 destination,
278 ConflictKind::SameFile,
279 ))
280 .await
281 {
282 ConflictResolution::Abort => Plan::Abort,
283 _ => Plan::Skip,
284 };
285 }
286 let Ok(existing) = fs::symlink_metadata(&destination) else {
287 return Plan::Publish {
288 destination,
289 replace: false,
290 };
291 };
292 let kind = if existing.is_dir() {
293 ConflictKind::DirectoryExists
294 } else {
295 ConflictKind::FileExists
296 };
297 let answer = match self.global {
298 Some(answer) => answer,
299 None => {
300 let answer = self
301 .ask(Conflict::new(source.into(), destination.clone(), kind))
302 .await;
303 if answer.is_global() {
304 self.global = Some(answer);
305 }
306 answer
307 }
308 };
309 match answer.to_single() {
310 ConflictResolution::Abort => Plan::Abort,
311 ConflictResolution::Overwrite => Plan::Publish {
312 destination,
313 replace: true,
314 },
315 ConflictResolution::AutoRename => Plan::Publish {
316 destination: auto_rename_path(&destination),
317 replace: false,
318 },
319 _ => Plan::Skip,
320 }
321 }
322
323 async fn ask(&mut self, conflict: Conflict) -> ConflictResolution {
324 request_resolution(&self.tx, conflict, TransferResult::Conflict, &self.token).await
325 }
326
327 async fn publish(&mut self, source: PathBuf, destination: PathBuf, replace: bool) {
328 self.progress.set_current_file(Some(source.clone()));
329 self.report();
330 let kind = self.kind;
331 let options = self.options;
332 let token = self.token.clone();
333 let bytes = Arc::new(AtomicU64::new(0));
334 let job = {
335 let (source, destination, bytes) = (source.clone(), destination.clone(), bytes.clone());
336 tokio::task::spawn_blocking(move || {
337 transfer_item(
338 kind,
339 &source,
340 &destination,
341 replace,
342 options,
343 &token,
344 &bytes,
345 )
346 })
347 };
348 let base = self.progress.bytes_processed;
349 let outcome = self.wait(job, base, &bytes).await;
350 self.progress.bytes_processed = base.saturating_add(bytes.load(Ordering::Relaxed));
351 self.progress.files_completed += 1;
352 match outcome {
353 Ok(ItemOutcome { errors }) => {
354 self.succeeded += 1;
355 for error in errors {
356 self.progress.add_error(error);
357 }
358 if replace {
359 self.replaced.push((source, destination));
360 } else {
361 self.completed.push((source, destination));
362 }
363 }
364 Err(error) => {
365 self.failed += 1;
366 self.progress.add_error(error);
367 }
368 }
369 self.report();
370 }
371
372 async fn wait(
374 &mut self,
375 mut job: tokio::task::JoinHandle<Result<ItemOutcome, OperationError>>,
376 base: u64,
377 bytes: &AtomicU64,
378 ) -> Result<ItemOutcome, OperationError> {
379 let mut ticker = tokio::time::interval(PROGRESS_INTERVAL);
380 ticker.tick().await;
381 loop {
382 tokio::select! {
383 result = &mut job => {
384 return result.unwrap_or_else(|error| {
385 Err(OperationError::new(PathBuf::new(), format!("worker failed: {error}")))
386 });
387 }
388 _ = ticker.tick() => {
389 self.progress.bytes_processed = base.saturating_add(bytes.load(Ordering::Relaxed));
390 self.report();
391 }
392 }
393 }
394 }
395
396 fn report(&self) {
397 publish_progress(
398 &self.tx,
399 || TransferResult::Progress(self.progress.clone()),
400 &self.token,
401 );
402 }
403
404 fn fail(&mut self, path: &Path, message: impl Into<String>) {
405 self.failed += 1;
406 self.progress
407 .add_error(OperationError::new(path.to_path_buf(), message));
408 }
409
410 async fn finish(self, aborted: bool) {
411 let complete = TransferComplete {
412 inner: OperationComplete {
413 cancelled: aborted || self.token.is_cancelled(),
414 operation_type: self.kind.operation_type(),
415 succeeded: self.succeeded,
416 failed: self.failed,
417 skipped: self.skipped,
418 bytes_processed: self.progress.bytes_processed,
419 errors: self.progress.errors,
420 },
421 completed: self.completed,
422 replaced: self.replaced,
423 };
424 let _ = self.tx.send(TransferResult::Complete(complete)).await;
425 }
426}
427
428struct ItemOutcome {
429 errors: Vec<OperationError>,
431}
432
433fn transfer_item(
434 kind: Kind,
435 source: &Path,
436 destination: &Path,
437 replace: bool,
438 options: TransferOptions,
439 token: &CancellationToken,
440 bytes: &AtomicU64,
441) -> Result<ItemOutcome, OperationError> {
442 let error = |message: String| OperationError::new(source.to_path_buf(), message);
443 let preserve = Preserve {
444 times: options.preserve_timestamps,
445 xattrs: options.preserve_xattrs,
446 };
447 let replace = replace.then_some(options.replace_policy);
448
449 if kind == Kind::Move {
450 if let Some(policy) = replace
451 && fs::symlink_metadata(destination).is_ok()
452 && !moves_across_devices(source, destination)
453 {
454 dispose(destination, policy).map_err(|e| error(e.to_string()))?;
455 }
456 match rename_noreplace(source, destination) {
457 Ok(()) => return Ok(ItemOutcome { errors: Vec::new() }),
458 Err(RenameError::CrossDevice) => {}
459 Err(RenameError::DestinationExists) => {
460 return Err(error(format!("'{}' already exists", destination.display())));
461 }
462 Err(RenameError::Io(e)) => return Err(error(format!("move failed: {e}"))),
463 }
464 }
465
466 let mut transfer = Transfer::new(token, bytes, preserve);
467 transfer
468 .copy_staged(source, destination, replace)
469 .map_err(|e| error(e.to_string()))?;
470 let errors = transfer.into_errors();
471 if kind == Kind::Move {
472 if !errors.is_empty() {
473 let mut errors = errors;
474 errors.push(error(
475 "some entries could not be copied; the source was kept".to_string(),
476 ));
477 return Ok(ItemOutcome { errors });
478 }
479 remove_path(source).map_err(|e| {
480 error(format!(
481 "copied to '{}', but removing the source failed: {e}",
482 destination.display()
483 ))
484 })?;
485 }
486 Ok(ItemOutcome { errors })
487}
488
489#[cfg(unix)]
490fn moves_across_devices(source: &Path, destination: &Path) -> bool {
491 use std::os::unix::fs::MetadataExt;
492 let parent = crate::fs::parent_or_current(destination);
493 match (fs::symlink_metadata(source), fs::metadata(parent)) {
494 (Ok(source), Ok(parent)) => source.dev() != parent.dev(),
495 _ => false,
496 }
497}
498
499#[cfg(not(unix))]
500fn moves_across_devices(_source: &Path, _destination: &Path) -> bool {
501 false
502}
503
504fn total_bytes(sources: &[PathBuf], token: &CancellationToken) -> u64 {
506 let mut total = 0u64;
507 let mut pending: Vec<PathBuf> = sources.to_vec();
508 while let Some(path) = pending.pop() {
509 if token.is_cancelled() {
510 break;
511 }
512 let Ok(metadata) = fs::symlink_metadata(&path) else {
513 continue;
514 };
515 if metadata.is_dir() {
516 if let Ok(entries) = fs::read_dir(&path) {
517 pending.extend(entries.flatten().map(|entry| entry.path()));
518 }
519 } else if metadata.is_file() {
520 total = total.saturating_add(metadata.len());
521 }
522 }
523 total
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 async fn drain(
531 mut rx: mpsc::Receiver<TransferResult>,
532 answer: ConflictResolution,
533 ) -> TransferComplete {
534 while let Some(message) = rx.recv().await {
535 match message {
536 TransferResult::Progress(_) => {}
537 TransferResult::Conflict(request) => request.resolve(answer),
538 TransferResult::Complete(complete) => return complete,
539 }
540 }
541 panic!("no completion");
542 }
543
544 fn options() -> TransferOptions {
545 TransferOptions {
546 replace_policy: ReplacePolicy::Permanent,
547 ..TransferOptions::default()
548 }
549 }
550
551 #[tokio::test]
552 async fn copy_reports_created_paths_and_bytes() {
553 let dir = tempfile::tempdir().unwrap();
554 let source = dir.path().join("a.txt");
555 fs::write(&source, "hello").unwrap();
556 let destination = dir.path().join("out");
557 let complete = drain(
558 start_copy(
559 vec![source.clone()],
560 destination.clone(),
561 options(),
562 CancellationToken::new(),
563 ),
564 ConflictResolution::Skip,
565 )
566 .await;
567 assert!(complete.inner.is_success(), "{:?}", complete.inner);
568 assert_eq!(complete.inner.bytes_processed, 5);
569 assert_eq!(
570 complete.completed,
571 vec![(source.clone(), destination.join("a.txt"))]
572 );
573 assert!(source.exists());
574 }
575
576 #[tokio::test]
577 async fn move_renames_and_skips_on_conflict() {
578 let dir = tempfile::tempdir().unwrap();
579 let destination = dir.path().join("out");
580 fs::create_dir(&destination).unwrap();
581 let (a, b) = (dir.path().join("a"), dir.path().join("b"));
582 fs::write(&a, "a").unwrap();
583 fs::write(&b, "b").unwrap();
584 fs::write(destination.join("b"), "existing").unwrap();
585 let complete = drain(
586 start_move(
587 vec![a.clone(), b.clone()],
588 destination.clone(),
589 options(),
590 CancellationToken::new(),
591 ),
592 ConflictResolution::Skip,
593 )
594 .await;
595 assert_eq!((complete.inner.succeeded, complete.inner.skipped), (1, 1));
596 assert!(!a.exists() && b.exists());
597 assert_eq!(
598 fs::read_to_string(destination.join("b")).unwrap(),
599 "existing"
600 );
601 }
602
603 #[tokio::test]
604 async fn overwrite_and_auto_rename() {
605 let dir = tempfile::tempdir().unwrap();
606 let destination = dir.path().join("out");
607 fs::create_dir(&destination).unwrap();
608 let source = dir.path().join("f");
609 fs::write(&source, "new").unwrap();
610 fs::write(destination.join("f"), "old").unwrap();
611 let complete = drain(
612 start_copy(
613 vec![source.clone()],
614 destination.clone(),
615 options(),
616 CancellationToken::new(),
617 ),
618 ConflictResolution::AutoRename,
619 )
620 .await;
621 assert_eq!(complete.completed[0].1, destination.join("f (1)"));
622 let complete = drain(
623 start_copy(
624 vec![source.clone()],
625 destination.clone(),
626 options(),
627 CancellationToken::new(),
628 ),
629 ConflictResolution::Overwrite,
630 )
631 .await;
632 assert!(complete.completed.is_empty());
633 assert_eq!(complete.replaced, [(source.clone(), destination.join("f"))]);
634 assert_eq!(complete.touched_paths(false), [destination.join("f")]);
635 assert_eq!(
636 complete.touched_paths(true),
637 [source, destination.join("f")]
638 );
639 assert_eq!(fs::read_to_string(destination.join("f")).unwrap(), "new");
640 }
641
642 #[tokio::test]
643 async fn refuses_to_copy_a_directory_into_itself() {
644 let dir = tempfile::tempdir().unwrap();
645 let source = dir.path().join("tree");
646 fs::create_dir(&source).unwrap();
647 let complete = drain(
648 start_copy(
649 vec![source.clone()],
650 source.join("inner"),
651 options(),
652 CancellationToken::new(),
653 ),
654 ConflictResolution::Skip,
655 )
656 .await;
657 assert_eq!(complete.inner.failed, 1);
658 assert!(!source.join("inner").exists());
659 }
660}