1mod error;
9#[cfg(unix)]
10mod hardlink;
11
12use clap::builder::ValueParser;
13use clap::error::ErrorKind;
14use clap::{Arg, ArgAction, ArgMatches, Command};
15use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
16
17#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::env;
21use std::ffi::OsString;
22use std::fs;
23use std::io;
24#[cfg(unix)]
25use std::os::unix;
26#[cfg(unix)]
27use std::os::unix::fs::FileTypeExt;
28#[cfg(windows)]
29use std::os::windows;
30use std::path::{Path, PathBuf, absolute};
31
32#[cfg(unix)]
33use crate::hardlink::{
34 HardlinkGroupScanner, HardlinkOptions, HardlinkTracker, create_hardlink_context,
35 with_optional_hardlink_context,
36};
37use uucore::backup_control::{self, source_is_target_backup};
38use uucore::display::Quotable;
39use uucore::error::{FromIo, UResult, USimpleError, UUsageError, set_exit_code};
40#[cfg(unix)]
41use uucore::fs::make_fifo;
42use uucore::fs::{
43 MissingHandling, ResolveMode, are_hardlinks_or_one_way_symlink_to_same_file,
44 are_hardlinks_to_same_file, canonicalize, path_ends_with_terminator,
45};
46#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
47use uucore::fsxattr;
48#[cfg(feature = "selinux")]
49use uucore::selinux::set_selinux_security_context;
50use uucore::translate;
51use uucore::update_control;
52
53pub use uucore::{backup_control::BackupMode, update_control::UpdateMode};
56use uucore::{format_usage, prompt_yes, show};
57
58use fs_extra::dir::get_size as dir_get_size;
59
60use crate::error::MvError;
61
62#[derive(Debug, Clone, Eq, PartialEq)]
70pub struct Options {
71 pub overwrite: OverwriteMode,
76
77 pub backup: BackupMode,
79
80 pub suffix: String,
82
83 pub update: UpdateMode,
85
86 pub target_dir: Option<OsString>,
89
90 pub no_target_dir: bool,
93
94 pub verbose: bool,
96
97 pub strip_slashes: bool,
99
100 pub progress_bar: bool,
102
103 pub debug: bool,
105
106 pub context: Option<String>,
108}
109
110impl Default for Options {
111 fn default() -> Self {
112 Self {
113 overwrite: OverwriteMode::default(),
114 backup: BackupMode::default(),
115 suffix: backup_control::DEFAULT_BACKUP_SUFFIX.to_owned(),
116 update: UpdateMode::default(),
117 target_dir: None,
118 no_target_dir: false,
119 verbose: false,
120 strip_slashes: false,
121 progress_bar: false,
122 debug: false,
123 context: None,
124 }
125 }
126}
127
128#[derive(Clone, Debug, Eq, PartialEq, Default)]
130pub enum OverwriteMode {
131 NoClobber,
133 Interactive,
135 #[default]
137 Force,
138}
139
140static OPT_FORCE: &str = "force";
141static OPT_INTERACTIVE: &str = "interactive";
142static OPT_NO_CLOBBER: &str = "no-clobber";
143static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes";
144static OPT_TARGET_DIRECTORY: &str = "target-directory";
145static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
146static OPT_VERBOSE: &str = "verbose";
147static OPT_PROGRESS: &str = "progress";
148static ARG_FILES: &str = "files";
149static OPT_DEBUG: &str = "debug";
150static OPT_CONTEXT: &str = "context";
151static OPT_SELINUX: &str = "selinux";
152
153#[uucore::main]
154pub fn uumain(args: impl uucore::Args) -> UResult<()> {
155 let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
156
157 let files: Vec<OsString> = matches
158 .get_many::<OsString>(ARG_FILES)
159 .unwrap_or_default()
160 .cloned()
161 .collect();
162
163 if files.len() == 1 && !matches.contains_id(OPT_TARGET_DIRECTORY) {
164 let err = uu_app().error(
165 ErrorKind::TooFewValues,
166 translate!("mv-error-insufficient-arguments", "arg_files" => ARG_FILES),
167 );
168 uucore::clap_localization::handle_clap_error_with_exit_code(err, 1);
169 }
170
171 let overwrite_mode = determine_overwrite_mode(&matches);
172 let backup_mode = backup_control::determine_backup_mode(&matches)?;
173 let update_mode = update_control::determine_update_mode(&matches);
174
175 if backup_mode != BackupMode::None
176 && (overwrite_mode == OverwriteMode::NoClobber
177 || update_mode == UpdateMode::None
178 || update_mode == UpdateMode::NoneFail)
179 {
180 return Err(UUsageError::new(
181 1,
182 translate!("mv-error-backup-with-no-clobber"),
183 ));
184 }
185
186 let backup_suffix = backup_control::determine_backup_suffix(&matches);
187
188 let target_dir = matches
189 .get_one::<OsString>(OPT_TARGET_DIRECTORY)
190 .map(OsString::from);
191
192 if let Some(ref maybe_dir) = target_dir {
193 if !Path::new(&maybe_dir).is_dir() {
194 return Err(MvError::TargetNotADirectory(maybe_dir.quote().to_string()).into());
195 }
196 }
197
198 let context = if matches.get_flag(OPT_SELINUX) {
202 Some(String::new())
203 } else {
204 matches.get_one::<String>(OPT_CONTEXT).cloned()
205 };
206
207 let opts = Options {
208 overwrite: overwrite_mode,
209 backup: backup_mode,
210 suffix: backup_suffix,
211 update: update_mode,
212 target_dir,
213 no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
214 verbose: matches.get_flag(OPT_VERBOSE) || matches.get_flag(OPT_DEBUG),
215 strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
216 progress_bar: matches.get_flag(OPT_PROGRESS),
217 debug: matches.get_flag(OPT_DEBUG),
218 context,
219 };
220
221 mv(&files[..], &opts)
222}
223
224pub fn uu_app() -> Command {
225 Command::new(uucore::util_name())
226 .version(uucore::crate_version!())
227 .about(translate!("mv-about"))
228 .help_template(uucore::localized_help_template(uucore::util_name()))
229 .override_usage(format_usage(&translate!("mv-usage")))
230 .after_help(format!(
231 "{}\n\n{}",
232 translate!("mv-after-help"),
233 backup_control::BACKUP_CONTROL_LONG_HELP
234 ))
235 .infer_long_args(true)
236 .arg(
237 Arg::new(OPT_FORCE)
238 .short('f')
239 .long(OPT_FORCE)
240 .help(translate!("mv-help-force"))
241 .overrides_with_all([OPT_INTERACTIVE, OPT_NO_CLOBBER])
242 .action(ArgAction::SetTrue),
243 )
244 .arg(
245 Arg::new(OPT_INTERACTIVE)
246 .short('i')
247 .long(OPT_INTERACTIVE)
248 .help(translate!("mv-help-interactive"))
249 .overrides_with_all([OPT_FORCE, OPT_NO_CLOBBER])
250 .action(ArgAction::SetTrue),
251 )
252 .arg(
253 Arg::new(OPT_NO_CLOBBER)
254 .short('n')
255 .long(OPT_NO_CLOBBER)
256 .help(translate!("mv-help-no-clobber"))
257 .overrides_with_all([OPT_FORCE, OPT_INTERACTIVE])
258 .action(ArgAction::SetTrue),
259 )
260 .arg(
261 Arg::new(OPT_STRIP_TRAILING_SLASHES)
262 .long(OPT_STRIP_TRAILING_SLASHES)
263 .help(translate!("mv-help-strip-trailing-slashes"))
264 .action(ArgAction::SetTrue),
265 )
266 .arg(backup_control::arguments::backup())
267 .arg(backup_control::arguments::backup_no_args())
268 .arg(backup_control::arguments::suffix())
269 .arg(update_control::arguments::update())
270 .arg(update_control::arguments::update_no_args())
271 .arg(
272 Arg::new(OPT_TARGET_DIRECTORY)
273 .short('t')
274 .long(OPT_TARGET_DIRECTORY)
275 .help(translate!("mv-help-target-directory"))
276 .value_name("DIRECTORY")
277 .value_hint(clap::ValueHint::DirPath)
278 .conflicts_with(OPT_NO_TARGET_DIRECTORY)
279 .value_parser(ValueParser::os_string()),
280 )
281 .arg(
282 Arg::new(OPT_NO_TARGET_DIRECTORY)
283 .short('T')
284 .long(OPT_NO_TARGET_DIRECTORY)
285 .help(translate!("mv-help-no-target-directory"))
286 .action(ArgAction::SetTrue),
287 )
288 .arg(
289 Arg::new(OPT_VERBOSE)
290 .short('v')
291 .long(OPT_VERBOSE)
292 .help(translate!("mv-help-verbose"))
293 .action(ArgAction::SetTrue),
294 )
295 .arg(
296 Arg::new(OPT_PROGRESS)
297 .short('g')
298 .long(OPT_PROGRESS)
299 .help(translate!("mv-help-progress"))
300 .action(ArgAction::SetTrue),
301 )
302 .arg(
303 Arg::new(OPT_SELINUX)
304 .short('Z')
305 .help(translate!("mv-help-selinux"))
306 .action(ArgAction::SetTrue),
307 )
308 .arg(
309 Arg::new(OPT_CONTEXT)
310 .long(OPT_CONTEXT)
311 .value_name("CTX")
312 .value_parser(clap::value_parser!(String))
313 .help(translate!("mv-help-context"))
314 .num_args(0..=1)
315 .require_equals(true)
316 .default_missing_value(""),
317 )
318 .arg(
319 Arg::new(ARG_FILES)
320 .action(ArgAction::Append)
321 .num_args(1..)
322 .required(true)
323 .value_parser(ValueParser::os_string())
324 .value_hint(clap::ValueHint::AnyPath),
325 )
326 .arg(
327 Arg::new(OPT_DEBUG)
328 .long(OPT_DEBUG)
329 .help(translate!("mv-help-debug"))
330 .action(ArgAction::SetTrue),
331 )
332}
333
334fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
335 if matches.get_flag(OPT_NO_CLOBBER) {
341 OverwriteMode::NoClobber
342 } else if matches.get_flag(OPT_INTERACTIVE) {
343 OverwriteMode::Interactive
344 } else {
345 OverwriteMode::Force
346 }
347}
348
349fn parse_paths(files: &[OsString], opts: &Options) -> Vec<PathBuf> {
350 let paths = files.iter().map(Path::new);
351
352 if opts.strip_slashes {
353 paths
354 .map(|p| p.components().as_path().to_owned())
355 .collect::<Vec<PathBuf>>()
356 } else {
357 paths.map(|p| p.to_owned()).collect::<Vec<PathBuf>>()
358 }
359}
360
361fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> {
362 if opts.backup == BackupMode::Simple && source_is_target_backup(source, target, &opts.suffix) {
363 return Err(io::Error::new(
364 io::ErrorKind::NotFound,
365 translate!("mv-error-backup-might-destroy-source", "target" => target.quote(), "source" => source.quote()),
366 )
367 .into());
368 }
369 if source.symlink_metadata().is_err() {
370 return Err(if path_ends_with_terminator(source) {
371 MvError::CannotStatNotADirectory(source.quote().to_string()).into()
372 } else {
373 MvError::NoSuchFile(source.quote().to_string()).into()
374 });
375 }
376
377 let source_is_dir = source.is_dir() && !source.is_symlink();
378 let target_is_dir = if target.is_symlink() {
379 fs::canonicalize(target).is_ok_and(|p| p.is_dir())
380 } else {
381 target.is_dir()
382 };
383
384 if path_ends_with_terminator(target)
385 && (!target_is_dir && !source_is_dir)
386 && !opts.no_target_dir
387 && opts.update != UpdateMode::IfOlder
388 {
389 return Err(MvError::FailedToAccessNotADirectory(target.quote().to_string()).into());
390 }
391
392 assert_not_same_file(source, target, target_is_dir, opts)?;
393
394 if target_is_dir {
395 if opts.no_target_dir {
396 if source.is_dir() {
397 #[cfg(unix)]
398 let (mut hardlink_tracker, hardlink_scanner) = create_hardlink_context();
399 #[cfg(unix)]
400 let hardlink_params = (Some(&mut hardlink_tracker), Some(&hardlink_scanner));
401 #[cfg(not(unix))]
402 let hardlink_params = (None, None);
403
404 rename(
405 source,
406 target,
407 opts,
408 None,
409 hardlink_params.0,
410 hardlink_params.1,
411 )
412 .map_err_context(|| {
413 translate!("mv-error-cannot-move", "source" => source.quote(), "target" => target.quote())
414 })
415 } else {
416 Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
417 }
418 } else {
419 move_files_into_dir(&[source.to_path_buf()], target, opts)
420 }
421 } else if target.exists() && source_is_dir {
422 match opts.overwrite {
423 OverwriteMode::NoClobber => return Ok(()),
424 OverwriteMode::Interactive => {
425 if !prompt_yes!(
426 "{}",
427 translate!("mv-prompt-overwrite", "target" => target.quote())
428 ) {
429 return Err(io::Error::other("").into());
430 }
431 }
432 OverwriteMode::Force => {}
433 }
434 Err(MvError::NonDirectoryToDirectory(
435 source.quote().to_string(),
436 target.quote().to_string(),
437 )
438 .into())
439 } else {
440 #[cfg(unix)]
441 let (mut hardlink_tracker, hardlink_scanner) = create_hardlink_context();
442 #[cfg(unix)]
443 let hardlink_params = (Some(&mut hardlink_tracker), Some(&hardlink_scanner));
444 #[cfg(not(unix))]
445 let hardlink_params = (None, None);
446
447 rename(
448 source,
449 target,
450 opts,
451 None,
452 hardlink_params.0,
453 hardlink_params.1,
454 )
455 .map_err(|e| USimpleError::new(1, format!("{e}")))
456 }
457}
458
459fn assert_not_same_file(
460 source: &Path,
461 target: &Path,
462 target_is_dir: bool,
463 opts: &Options,
464) -> UResult<()> {
465 let canonicalized_source = match canonicalize(
467 absolute(source)?,
468 MissingHandling::Normal,
469 ResolveMode::Logical,
470 ) {
471 Ok(source) if source.exists() => source,
472 _ => absolute(source)?, };
474
475 let target_is_dir = target_is_dir && !opts.no_target_dir;
477 let canonicalized_target = if target_is_dir {
478 canonicalize(
481 absolute(target)?,
482 MissingHandling::Normal,
483 ResolveMode::Logical,
484 )?
485 .join(source.file_name().unwrap_or_default())
486 } else {
487 match absolute(target)?.parent() {
490 Some(parent) if parent.to_str() != Some("") => {
491 canonicalize(parent, MissingHandling::Normal, ResolveMode::Logical)?
492 .join(target.file_name().unwrap_or_default())
493 }
494 _ => absolute(target)?, }
497 };
498
499 let same_file = (canonicalized_source.eq(&canonicalized_target)
500 || are_hardlinks_to_same_file(source, target)
501 || are_hardlinks_or_one_way_symlink_to_same_file(source, target))
502 && opts.backup == BackupMode::None;
503
504 let target_display = match source.file_name() {
507 Some(file_name) if target_is_dir => {
508 let mut path = target
510 .display()
511 .to_string()
512 .trim_end_matches('/')
513 .to_owned();
514
515 path.push('/');
516 path.push_str(&file_name.to_string_lossy());
517
518 path.quote().to_string()
519 }
520 _ => target.quote().to_string(),
521 };
522
523 if same_file
524 && (canonicalized_source.eq(&canonicalized_target)
525 || source.eq(Path::new("."))
526 || source.ends_with("/.")
527 || source.is_file())
528 {
529 return Err(MvError::SameFile(source.quote().to_string(), target_display).into());
530 } else if (same_file || canonicalized_target.starts_with(canonicalized_source))
531 && !source.is_symlink()
533 {
534 return Err(
535 MvError::SelfTargetSubdirectory(source.quote().to_string(), target_display).into(),
536 );
537 }
538 Ok(())
539}
540
541fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> {
542 if opts.no_target_dir {
543 return Err(UUsageError::new(
544 1,
545 translate!("mv-error-extra-operand", "operand" => paths.last().unwrap().quote()),
546 ));
547 }
548 let target_dir = paths.last().unwrap();
549 let sources = &paths[..paths.len() - 1];
550
551 move_files_into_dir(sources, target_dir, opts)
552}
553
554pub fn mv(files: &[OsString], opts: &Options) -> UResult<()> {
558 let paths = parse_paths(files, opts);
559
560 if let Some(ref name) = opts.target_dir {
561 return move_files_into_dir(&paths, &PathBuf::from(name), opts);
562 }
563
564 match paths.len() {
565 2 => handle_two_paths(&paths[0], &paths[1], opts),
566 _ => handle_multiple_paths(&paths, opts),
567 }
568}
569
570#[allow(clippy::cognitive_complexity)]
571fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, options: &Options) -> UResult<()> {
572 let mut moved_destinations: HashSet<PathBuf> = HashSet::with_capacity(files.len());
574 #[cfg(unix)]
576 let (mut hardlink_tracker, hardlink_scanner) = {
577 let (tracker, mut scanner) = create_hardlink_context();
578
579 let hardlink_options = HardlinkOptions {
581 verbose: options.verbose || options.debug,
582 };
583
584 if let Err(e) = scanner.scan_files(files, &hardlink_options) {
586 if hardlink_options.verbose {
587 eprintln!("mv: warning: failed to scan files for hardlinks: {e}");
588 eprintln!("mv: continuing without hardlink preservation");
589 } else {
590 eprintln!(
592 "mv: warning: hardlink scanning failed, continuing without hardlink preservation"
593 );
594 }
595 }
598
599 (tracker, scanner)
600 };
601
602 if !target_dir.is_dir() {
603 return Err(MvError::NotADirectory(target_dir.quote().to_string()).into());
604 }
605
606 let display_manager = options.progress_bar.then(MultiProgress::new);
607
608 let count_progress = if let Some(ref display_manager) = display_manager {
609 if files.len() > 1 {
610 Some(
611 display_manager.add(
612 ProgressBar::new(files.len().try_into().unwrap()).with_style(
613 ProgressStyle::with_template(&format!(
614 "{} {{msg}} {{wide_bar}} {{pos}}/{{len}}",
615 translate!("mv-progress-moving")
616 ))
617 .unwrap(),
618 ),
619 ),
620 )
621 } else {
622 None
623 }
624 } else {
625 None
626 };
627
628 for sourcepath in files {
629 if sourcepath.symlink_metadata().is_err() {
630 show!(MvError::NoSuchFile(sourcepath.quote().to_string()));
631 continue;
632 }
633
634 if let Some(ref pb) = count_progress {
635 let msg = format!("{} (scanning hardlinks)", sourcepath.to_string_lossy());
636 pb.set_message(msg);
637 }
638
639 let targetpath = match sourcepath.file_name() {
640 Some(name) => target_dir.join(name),
641 None => {
642 show!(MvError::NoSuchFile(sourcepath.quote().to_string()));
643 continue;
644 }
645 };
646
647 if moved_destinations.contains(&targetpath) && options.backup != BackupMode::Numbered {
648 show!(USimpleError::new(
650 1,
651 translate!("mv-error-will-not-overwrite-just-created", "target" => targetpath.display(), "source" => sourcepath.display()),
652 ));
653 continue;
654 }
655
656 if let Err(e) = assert_not_same_file(sourcepath, target_dir, true, options) {
659 show!(e);
660 continue;
661 }
662
663 #[cfg(unix)]
664 let hardlink_params = (Some(&mut hardlink_tracker), Some(&hardlink_scanner));
665 #[cfg(not(unix))]
666 let hardlink_params = (None, None);
667
668 match rename(
669 sourcepath,
670 &targetpath,
671 options,
672 display_manager.as_ref(),
673 hardlink_params.0,
674 hardlink_params.1,
675 ) {
676 Err(e) if e.to_string().is_empty() => set_exit_code(1),
677 Err(e) => {
678 let e = e.map_err_context(|| {
679 translate!("mv-error-cannot-move", "source" => sourcepath.quote(), "target" => targetpath.quote())
680 });
681 match display_manager {
682 Some(ref pb) => pb.suspend(|| show!(e)),
683 None => show!(e),
684 }
685 }
686 Ok(()) => (),
687 }
688 if let Some(ref pb) = count_progress {
689 pb.inc(1);
690 }
691 moved_destinations.insert(targetpath.clone());
692 }
693 Ok(())
694}
695
696fn rename(
697 from: &Path,
698 to: &Path,
699 opts: &Options,
700 display_manager: Option<&MultiProgress>,
701 #[cfg(unix)] hardlink_tracker: Option<&mut HardlinkTracker>,
702 #[cfg(unix)] hardlink_scanner: Option<&HardlinkGroupScanner>,
703 #[cfg(not(unix))] _hardlink_tracker: Option<()>,
704 #[cfg(not(unix))] _hardlink_scanner: Option<()>,
705) -> io::Result<()> {
706 let mut backup_path = None;
707
708 if to.exists() {
709 if opts.update == UpdateMode::None {
710 if opts.debug {
711 println!("{}", translate!("mv-debug-skipped", "target" => to.quote()));
712 }
713 return Ok(());
714 }
715
716 if (opts.update == UpdateMode::IfOlder)
717 && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()?
718 {
719 return Ok(());
720 }
721
722 if opts.update == UpdateMode::NoneFail {
723 let err_msg = translate!("mv-error-not-replacing", "target" => to.quote());
724 return Err(io::Error::other(err_msg));
725 }
726
727 match opts.overwrite {
728 OverwriteMode::NoClobber => {
729 if opts.debug {
730 println!("{}", translate!("mv-debug-skipped", "target" => to.quote()));
731 }
732 return Ok(());
733 }
734 OverwriteMode::Interactive => {
735 if !prompt_yes!(
736 "{}",
737 translate!("mv-prompt-overwrite", "target" => to.quote())
738 ) {
739 return Err(io::Error::other(""));
740 }
741 }
742 OverwriteMode::Force => {}
743 }
744
745 backup_path = backup_control::get_backup_path(opts.backup, to, &opts.suffix);
746 if let Some(ref backup_path) = backup_path {
747 rename_with_fallback(to, backup_path, display_manager, false, None, None)?;
749 }
750 }
751
752 if to.exists() && to.is_dir() && !to.is_symlink() {
754 if from.is_dir() {
756 if is_empty_dir(to) {
757 fs::remove_dir(to)?;
758 } else {
759 return Err(io::Error::other(translate!("mv-error-directory-not-empty")));
760 }
761 }
762 }
763
764 #[cfg(unix)]
765 {
766 rename_with_fallback(
767 from,
768 to,
769 display_manager,
770 opts.verbose,
771 hardlink_tracker,
772 hardlink_scanner,
773 )?;
774 }
775 #[cfg(not(unix))]
776 {
777 rename_with_fallback(from, to, display_manager, opts.verbose, None, None)?;
778 }
779
780 #[cfg(feature = "selinux")]
781 if let Some(ref context) = opts.context {
782 set_selinux_security_context(to, Some(context))
783 .map_err(|e| io::Error::other(e.to_string()))?;
784 }
785
786 if opts.verbose {
787 let message = match backup_path {
788 Some(path) => {
789 translate!("mv-verbose-renamed-with-backup", "from" => from.quote(), "to" => to.quote(), "backup" => path.quote())
790 }
791 None => translate!("mv-verbose-renamed", "from" => from.quote(), "to" => to.quote()),
792 };
793
794 match display_manager {
795 Some(pb) => pb.suspend(|| {
796 println!("{message}");
797 }),
798 None => println!("{message}"),
799 }
800 }
801 Ok(())
802}
803
804#[cfg(unix)]
805fn is_fifo(filetype: fs::FileType) -> bool {
806 filetype.is_fifo()
807}
808
809#[cfg(not(unix))]
810fn is_fifo(_filetype: fs::FileType) -> bool {
811 false
812}
813
814fn rename_with_fallback(
817 from: &Path,
818 to: &Path,
819 display_manager: Option<&MultiProgress>,
820 verbose: bool,
821 #[cfg(unix)] hardlink_tracker: Option<&mut HardlinkTracker>,
822 #[cfg(unix)] hardlink_scanner: Option<&HardlinkGroupScanner>,
823 #[cfg(not(unix))] _hardlink_tracker: Option<()>,
824 #[cfg(not(unix))] _hardlink_scanner: Option<()>,
825) -> io::Result<()> {
826 fs::rename(from, to).or_else(|err| {
827 #[cfg(windows)]
828 const EXDEV: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as _;
829 #[cfg(unix)]
830 const EXDEV: i32 = libc::EXDEV as _;
831
832 let should_fallback =
837 matches!(err.raw_os_error(), Some(EXDEV)) || (from.is_file() && can_delete_file(from));
838 if !should_fallback {
839 return Err(err);
840 }
841 let metadata = from.symlink_metadata()?;
843 let file_type = metadata.file_type();
844 if file_type.is_symlink() {
845 rename_symlink_fallback(from, to)
846 } else if file_type.is_dir() {
847 #[cfg(unix)]
848 {
849 with_optional_hardlink_context(
850 hardlink_tracker,
851 hardlink_scanner,
852 |tracker, scanner| {
853 rename_dir_fallback(
854 from,
855 to,
856 display_manager,
857 verbose,
858 Some(tracker),
859 Some(scanner),
860 )
861 },
862 )
863 }
864 #[cfg(not(unix))]
865 {
866 rename_dir_fallback(from, to, display_manager, verbose)
867 }
868 } else if is_fifo(file_type) {
869 rename_fifo_fallback(from, to)
870 } else {
871 #[cfg(unix)]
872 {
873 with_optional_hardlink_context(
874 hardlink_tracker,
875 hardlink_scanner,
876 |tracker, scanner| rename_file_fallback(from, to, Some(tracker), Some(scanner)),
877 )
878 }
879 #[cfg(not(unix))]
880 {
881 rename_file_fallback(from, to)
882 }
883 }
884 })
885}
886
887#[cfg(unix)]
889fn rename_fifo_fallback(from: &Path, to: &Path) -> io::Result<()> {
890 if to.try_exists()? {
891 fs::remove_file(to)?;
892 }
893 make_fifo(to).and_then(|_| fs::remove_file(from))
894}
895
896#[cfg(not(unix))]
897fn rename_fifo_fallback(_from: &Path, _to: &Path) -> io::Result<()> {
898 Ok(())
899}
900
901#[cfg(unix)]
904fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
905 let path_symlink_points_to = fs::read_link(from)?;
906 unix::fs::symlink(path_symlink_points_to, to).and_then(|_| fs::remove_file(from))
907}
908
909#[cfg(windows)]
910fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
911 let path_symlink_points_to = fs::read_link(from)?;
912 if path_symlink_points_to.exists() {
913 if path_symlink_points_to.is_dir() {
914 windows::fs::symlink_dir(&path_symlink_points_to, to)?;
915 } else {
916 windows::fs::symlink_file(&path_symlink_points_to, to)?;
917 }
918 fs::remove_file(from)
919 } else {
920 Err(io::Error::new(
921 io::ErrorKind::NotFound,
922 translate!("mv-error-dangling-symlink"),
923 ))
924 }
925}
926
927#[cfg(not(any(windows, unix)))]
928fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
929 let path_symlink_points_to = fs::read_link(from)?;
930 Err(io::Error::new(
931 io::ErrorKind::Other,
932 translate!("mv-error-no-symlink-support"),
933 ))
934}
935
936fn rename_dir_fallback(
937 from: &Path,
938 to: &Path,
939 display_manager: Option<&MultiProgress>,
940 verbose: bool,
941 #[cfg(unix)] hardlink_tracker: Option<&mut HardlinkTracker>,
942 #[cfg(unix)] hardlink_scanner: Option<&HardlinkGroupScanner>,
943) -> io::Result<()> {
944 if to.exists() {
948 fs::remove_dir_all(to)?;
949 }
950
951 let total_size = dir_get_size(from).ok();
957
958 let progress_bar = match (display_manager, total_size) {
959 (Some(display_manager), Some(total_size)) => {
960 let template = "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}";
961 let style = ProgressStyle::with_template(template).unwrap();
962 let bar = ProgressBar::new(total_size).with_style(style);
963 Some(display_manager.add(bar))
964 }
965 (_, _) => None,
966 };
967
968 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
969 let xattrs = fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| HashMap::new());
970
971 let result = copy_dir_contents(
973 from,
974 to,
975 #[cfg(unix)]
976 hardlink_tracker,
977 #[cfg(unix)]
978 hardlink_scanner,
979 verbose,
980 progress_bar.as_ref(),
981 display_manager,
982 );
983
984 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
985 fsxattr::apply_xattrs(to, xattrs)?;
986
987 result?;
988
989 fs::remove_dir_all(from)?;
991
992 Ok(())
993}
994
995fn copy_dir_contents(
997 from: &Path,
998 to: &Path,
999 #[cfg(unix)] hardlink_tracker: Option<&mut HardlinkTracker>,
1000 #[cfg(unix)] hardlink_scanner: Option<&HardlinkGroupScanner>,
1001 verbose: bool,
1002 progress_bar: Option<&ProgressBar>,
1003 display_manager: Option<&MultiProgress>,
1004) -> io::Result<()> {
1005 fs::create_dir_all(to)?;
1007
1008 #[cfg(unix)]
1010 {
1011 if let (Some(tracker), Some(scanner)) = (hardlink_tracker, hardlink_scanner) {
1012 copy_dir_contents_recursive(
1013 from,
1014 to,
1015 tracker,
1016 scanner,
1017 verbose,
1018 progress_bar,
1019 display_manager,
1020 )?;
1021 }
1022 }
1023 #[cfg(not(unix))]
1024 {
1025 copy_dir_contents_recursive(from, to, None, None, verbose, progress_bar, display_manager)?;
1026 }
1027
1028 Ok(())
1029}
1030
1031fn copy_dir_contents_recursive(
1032 from_dir: &Path,
1033 to_dir: &Path,
1034 #[cfg(unix)] hardlink_tracker: &mut HardlinkTracker,
1035 #[cfg(unix)] hardlink_scanner: &HardlinkGroupScanner,
1036 #[cfg(not(unix))] _hardlink_tracker: Option<()>,
1037 #[cfg(not(unix))] _hardlink_scanner: Option<()>,
1038 verbose: bool,
1039 progress_bar: Option<&ProgressBar>,
1040 display_manager: Option<&MultiProgress>,
1041) -> io::Result<()> {
1042 let entries = fs::read_dir(from_dir)?;
1043
1044 for entry in entries {
1045 let entry = entry?;
1046 let from_path = entry.path();
1047 let file_name = from_path.file_name().unwrap();
1048 let to_path = to_dir.join(file_name);
1049
1050 if let Some(pb) = progress_bar {
1051 pb.set_message(from_path.to_string_lossy().to_string());
1052 }
1053
1054 if from_path.is_dir() {
1055 fs::create_dir_all(&to_path)?;
1057
1058 if verbose {
1060 let message = translate!("mv-verbose-renamed", "from" => from_path.quote(), "to" => to_path.quote());
1061 match display_manager {
1062 Some(pb) => pb.suspend(|| {
1063 println!("{message}");
1064 }),
1065 None => println!("{message}"),
1066 }
1067 }
1068
1069 copy_dir_contents_recursive(
1070 &from_path,
1071 &to_path,
1072 #[cfg(unix)]
1073 hardlink_tracker,
1074 #[cfg(unix)]
1075 hardlink_scanner,
1076 #[cfg(not(unix))]
1077 _hardlink_tracker,
1078 #[cfg(not(unix))]
1079 _hardlink_scanner,
1080 verbose,
1081 progress_bar,
1082 display_manager,
1083 )?;
1084 } else {
1085 #[cfg(unix)]
1087 {
1088 copy_file_with_hardlinks_helper(
1089 &from_path,
1090 &to_path,
1091 hardlink_tracker,
1092 hardlink_scanner,
1093 )?;
1094 }
1095 #[cfg(not(unix))]
1096 {
1097 fs::copy(&from_path, &to_path)?;
1098 }
1099
1100 if verbose {
1102 let message = translate!("mv-verbose-renamed", "from" => from_path.quote(), "to" => to_path.quote());
1103 match display_manager {
1104 Some(pb) => pb.suspend(|| {
1105 println!("{message}");
1106 }),
1107 None => println!("{message}"),
1108 }
1109 }
1110 }
1111
1112 if let Some(pb) = progress_bar {
1113 if let Ok(metadata) = from_path.metadata() {
1114 pb.inc(metadata.len());
1115 }
1116 }
1117 }
1118
1119 Ok(())
1120}
1121
1122#[cfg(unix)]
1123fn copy_file_with_hardlinks_helper(
1124 from: &Path,
1125 to: &Path,
1126 hardlink_tracker: &mut HardlinkTracker,
1127 hardlink_scanner: &HardlinkGroupScanner,
1128) -> io::Result<()> {
1129 use crate::hardlink::HardlinkOptions;
1131 let hardlink_options = HardlinkOptions::default();
1132 if let Some(existing_target) =
1134 hardlink_tracker.check_hardlink(from, to, hardlink_scanner, &hardlink_options)?
1135 {
1136 fs::hard_link(&existing_target, to)?;
1137 return Ok(());
1138 }
1139
1140 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1142 {
1143 fs::copy(from, to).and_then(|_| fsxattr::copy_xattrs(&from, &to))?;
1144 }
1145 #[cfg(any(target_os = "macos", target_os = "redox"))]
1146 {
1147 fs::copy(from, to)?;
1148 }
1149
1150 Ok(())
1151}
1152
1153fn rename_file_fallback(
1154 from: &Path,
1155 to: &Path,
1156 #[cfg(unix)] hardlink_tracker: Option<&mut HardlinkTracker>,
1157 #[cfg(unix)] hardlink_scanner: Option<&HardlinkGroupScanner>,
1158) -> io::Result<()> {
1159 if to.is_symlink() {
1161 fs::remove_file(to).map_err(|err| {
1162 let inter_device_msg = translate!("mv-error-inter-device-move-failed", "from" => from.display(), "to" => to.display(), "err" => err);
1163 io::Error::new(err.kind(), inter_device_msg)
1164 })?;
1165 } else if to.exists() {
1166 fs::remove_file(to)?;
1168 }
1169
1170 #[cfg(unix)]
1172 {
1173 if let (Some(tracker), Some(scanner)) = (hardlink_tracker, hardlink_scanner) {
1174 use crate::hardlink::HardlinkOptions;
1175 let hardlink_options = HardlinkOptions::default();
1176 if let Some(existing_target) =
1177 tracker.check_hardlink(from, to, scanner, &hardlink_options)?
1178 {
1179 fs::hard_link(&existing_target, to)?;
1181 fs::remove_file(from)?;
1182 return Ok(());
1183 }
1184 }
1185 }
1186
1187 #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
1189 fs::copy(from, to)
1190 .and_then(|_| fsxattr::copy_xattrs(&from, &to))
1191 .and_then(|_| fs::remove_file(from))
1192 .map_err(|err| io::Error::new(err.kind(), translate!("mv-error-permission-denied")))?;
1193 #[cfg(any(target_os = "macos", target_os = "redox", not(unix)))]
1194 fs::copy(from, to)
1195 .and_then(|_| fs::remove_file(from))
1196 .map_err(|err| io::Error::new(err.kind(), translate!("mv-error-permission-denied")))?;
1197 Ok(())
1198}
1199
1200fn is_empty_dir(path: &Path) -> bool {
1201 fs::read_dir(path).is_ok_and(|mut contents| contents.next().is_none())
1202}
1203
1204#[cfg(windows)]
1206fn can_delete_file(path: &Path) -> bool {
1207 use std::{
1208 os::windows::ffi::OsStrExt as _,
1209 ptr::{null, null_mut},
1210 };
1211
1212 use windows_sys::Win32::{
1213 Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
1214 Storage::FileSystem::{
1215 CreateFileW, DELETE, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_SHARE_READ,
1216 FILE_SHARE_WRITE, OPEN_EXISTING,
1217 },
1218 };
1219
1220 let wide_path = path
1221 .as_os_str()
1222 .encode_wide()
1223 .chain([0])
1224 .collect::<Vec<u16>>();
1225
1226 let handle = unsafe {
1227 CreateFileW(
1228 wide_path.as_ptr(),
1229 DELETE,
1230 FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
1231 null(),
1232 OPEN_EXISTING,
1233 FILE_ATTRIBUTE_NORMAL,
1234 null_mut(),
1235 )
1236 };
1237
1238 if handle == INVALID_HANDLE_VALUE {
1239 return false;
1240 }
1241
1242 unsafe { CloseHandle(handle) };
1243
1244 true
1245}
1246
1247#[cfg(not(windows))]
1248fn can_delete_file(_: &Path) -> bool {
1249 false
1254}