diff --git a/Cargo.lock b/Cargo.lock index f15cf9372ae..3fc6e95488b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2991,9 +2991,12 @@ name = "uu_mv" version = "0.0.27" dependencies = [ "clap", + "filetime", "fs_extra", "indicatif", + "tempfile", "uucore", + "walkdir", ] [[package]] diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index afccf2fefd2..b06aeb02588 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -29,7 +29,7 @@ use platform::copy_on_write; use uucore::display::Quotable; use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError}; use uucore::fs::{ - are_hardlinks_to_same_file, canonicalize, get_filename, is_symlink_loop, + are_hardlinks_to_same_file, canonicalize, disk_usage, get_filename, is_symlink_loop, path_ends_with_terminator, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, }; @@ -2447,42 +2447,6 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu Ok(target.join(local_to_root)) } -/// Get the total size of a slice of files and directories. -/// -/// This function is much like the `du` utility, by recursively getting the sizes of files in directories. -/// Files are not deduplicated when appearing in multiple sources. If `recursive` is set to `false`, the -/// directories in `paths` will be ignored. -fn disk_usage(paths: &[PathBuf], recursive: bool) -> io::Result { - let mut total = 0; - for p in paths { - let md = fs::metadata(p)?; - if md.file_type().is_dir() { - if recursive { - total += disk_usage_directory(p)?; - } - } else { - total += md.len(); - } - } - Ok(total) -} - -/// A helper for `disk_usage` specialized for directories. -fn disk_usage_directory(p: &Path) -> io::Result { - let mut total = 0; - - for entry in fs::read_dir(p)? { - let entry = entry?; - if entry.file_type()?.is_dir() { - total += disk_usage_directory(&entry.path())?; - } else { - total += entry.metadata()?.len(); - } - } - - Ok(total) -} - #[cfg(test)] mod tests { diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index c484d5a77f3..cf3051326f5 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -20,13 +20,20 @@ path = "src/mv.rs" clap = { workspace = true } fs_extra = { workspace = true } indicatif = { workspace = true } +walkdir = { workspace = true } +filetime = { workspace = true } uucore = { workspace = true, features = [ "backup-control", "fs", "fsxattr", "update-control", + "perms", + "entries", ] } +[dev-dependencies] +tempfile = { workspace = true } + [[bin]] name = "mv" path = "src/main.rs" diff --git a/src/uu/mv/src/error.rs b/src/uu/mv/src/error.rs index f989d4e1332..9dc2974fdb7 100644 --- a/src/uu/mv/src/error.rs +++ b/src/uu/mv/src/error.rs @@ -7,6 +7,8 @@ use std::fmt::{Display, Formatter, Result}; use uucore::error::UError; +use fs_extra::error::Error as FsXError; + #[derive(Debug)] pub enum MvError { NoSuchFile(String), @@ -19,6 +21,8 @@ pub enum MvError { NotADirectory(String), TargetNotADirectory(String), FailedToAccessNotADirectory(String), + FsXError(FsXError), + NotAllFilesMoved, } impl Error for MvError {} @@ -49,6 +53,18 @@ impl Display for MvError { Self::FailedToAccessNotADirectory(t) => { write!(f, "failed to access {t}: Not a directory") } + Self::FsXError(err) => { + write!(f, "{err}") + } + Self::NotAllFilesMoved => { + write!(f, "failed to move all files") + } } } } + +impl From for MvError { + fn from(err: FsXError) -> Self { + Self::FsXError(err) + } +} diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 5f1b717834b..62c00626105 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -3,12 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) sourcepath targetpath nushell canonicalized +// spell-checker:ignore (ToDO) sourcepath targetpath nushell canonicalized lred mod error; use clap::builder::ValueParser; use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command}; +use filetime::set_symlink_file_times; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use std::collections::HashSet; use std::env; @@ -19,30 +20,37 @@ use std::io; use std::os::unix; #[cfg(windows)] use std::os::windows; -use std::path::{Path, PathBuf}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; +#[cfg(unix)] +use unix::fs::{FileTypeExt, MetadataExt}; use uucore::backup_control::{self, source_is_target_backup}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{ - are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, + are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file, disk_usage, path_ends_with_terminator, }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; -use uucore::update_control; +#[cfg(unix)] +use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel}; +use uucore::{show_error, update_control}; +use walkdir::WalkDir; // These are exposed for projects (e.g. nushell) that want to create an `Options` value, which // requires these enums pub use uucore::{backup_control::BackupMode, update_control::UpdateMode}; use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; -use fs_extra::dir::{ - get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions, - TransitProcess, TransitProcessResult, +use fs_extra::{ + error::{ErrorKind as FsXErrorKind, Result as FsXResult}, + file::{self, CopyOptions}, }; use crate::error::MvError; +type MvResult = Result; + /// Options contains all the possible behaviors and flags for mv. /// /// All options are public so that the options can be programmatically @@ -99,9 +107,81 @@ pub enum OverwriteMode { Force, } +/// a context for handling verbose output during file operations. +struct VerboseContext<'a> { + backup: Option<&'a Path>, + pb: Option<&'a MultiProgress>, +} + +impl<'a> VerboseContext<'a> { + fn new(backup: Option<&'a Path>, pb: Option<&'a MultiProgress>) -> Self { + VerboseContext { backup, pb } + } + + fn hide_pb_and_print(&self, msg: &str) { + match self.pb { + Some(pb) => pb.suspend(|| { + println!("{msg}"); + }), + None => println!("{msg}"), + }; + } + + fn print_move_file(&self, from: &Path, to: &Path) { + let message = match self.backup.as_ref() { + Some(path) => format!( + "renamed {} -> {} (backup: {})", + from.quote(), + to.quote(), + path.quote() + ), + None => format!("renamed {} -> {}", from.quote(), to.quote()), + }; + self.hide_pb_and_print(&message); + } + + fn print_copy_file(&self, from: &Path, to: &Path, with_backup_message: bool) { + let message = match self.backup.as_ref() { + Some(path) if with_backup_message => format!( + "copied {} -> {} (backup: {})", + from.quote(), + to.quote(), + path.quote() + ), + _ => format!("copied {} -> {}", from.quote(), to.quote()), + }; + self.hide_pb_and_print(&message); + } + + fn create_directory(&self, path: &Path) { + let message = format!( + "created directory {}", + path.to_string_lossy() + .trim_end_matches(MAIN_SEPARATOR) + .quote() + ); + self.hide_pb_and_print(&message); + } + + fn remove_file(&self, from: &Path) { + let message = format!("removed {}", from.quote()); + self.hide_pb_and_print(&message); + } + + fn remove_directory(&self, from: &Path) { + let message = format!("removed directory {}", from.quote()); + self.hide_pb_and_print(&message); + } +} + const ABOUT: &str = help_about!("mv.md"); const USAGE: &str = help_usage!("mv.md"); const AFTER_HELP: &str = help_section!("after help", "mv.md"); +// os error code for when rename operation crosses devices. +#[cfg(unix)] +const CROSSES_DEVICES_ERROR_CODE: i32 = 18; +#[cfg(target_os = "windows")] +const CROSSES_DEVICES_ERROR_CODE: i32 = 17; static OPT_FORCE: &str = "force"; static OPT_INTERACTIVE: &str = "interactive"; @@ -527,6 +607,18 @@ fn rename( ) -> io::Result<()> { let mut backup_path = None; + // If `no-target-directory` is specified, we treat the destination as a file. + // In that case, if there is a trailing forward slash, we remove it. + let to = if path_ends_with_terminator(to) && opts.no_target_dir { + let to_str = to.to_string_lossy(); + let trimmed_to = to_str.trim_end_matches(MAIN_SEPARATOR); + Path::new(trimmed_to).to_path_buf() + } else { + to.to_path_buf() + }; + + let to = &to; + if to.exists() { if opts.update == UpdateMode::ReplaceIfOlder && opts.overwrite == OverwriteMode::Interactive { @@ -570,7 +662,7 @@ fn rename( backup_path = backup_control::get_backup_path(opts.backup, to, &opts.suffix); if let Some(ref backup_path) = backup_path { - rename_with_fallback(to, backup_path, multi_progress)?; + rename_with_fallback(to, backup_path, multi_progress, None)?; } } @@ -586,27 +678,13 @@ fn rename( } } - rename_with_fallback(from, to, multi_progress)?; - - if opts.verbose { - let message = match backup_path { - Some(path) => format!( - "renamed {} -> {} (backup: {})", - from.quote(), - to.quote(), - path.quote() - ), - None => format!("renamed {} -> {}", from.quote(), to.quote()), - }; + let verbose_context = if opts.verbose { + Some(VerboseContext::new(backup_path.as_deref(), multi_progress)) + } else { + None + }; - match multi_progress { - Some(pb) => pb.suspend(|| { - println!("{message}"); - }), - None => println!("{message}"), - }; - } - Ok(()) + rename_with_fallback(from, to, multi_progress, verbose_context.as_ref()) } /// A wrapper around `fs::rename`, so that if it fails, we try falling back on @@ -615,14 +693,22 @@ fn rename_with_fallback( from: &Path, to: &Path, multi_progress: Option<&MultiProgress>, + verbose_context: Option<&VerboseContext<'_>>, ) -> io::Result<()> { - if fs::rename(from, to).is_err() { + if let Err(err) = fs::rename(from, to) { // Get metadata without following symlinks let metadata = from.symlink_metadata()?; let file_type = metadata.file_type(); if file_type.is_symlink() { rename_symlink_fallback(from, to)?; + if let Some(vc) = verbose_context { + vc.print_move_file(from, to); + } + } else if !matches!(err.raw_os_error(),Some(err_code)if err_code == CROSSES_DEVICES_ERROR_CODE) + { + // only try to copy if os reports an crosses devices error. + return Err(err); } else if file_type.is_dir() { // We remove the destination directory if it exists to match the // behavior of `fs::rename`. As far as I can tell, `fs_extra`'s @@ -630,58 +716,39 @@ fn rename_with_fallback( if to.exists() { fs::remove_dir_all(to)?; } - let options = DirCopyOptions { - // From the `fs_extra` documentation: - // "Recursively copy a directory with a new name or place it - // inside the destination. (same behaviors like cp -r in Unix)" - copy_inside: true, - ..DirCopyOptions::new() - }; // Calculate total size of directory // Silently degrades: // If finding the total size fails for whatever reason, // the progress bar wont be shown for this file / dir. // (Move will probably fail due to permission error later?) - let total_size = dir_get_size(from).ok(); - - let progress_bar = - if let (Some(multi_progress), Some(total_size)) = (multi_progress, total_size) { + let mut progress_bar = None; + if let Some(multi_progress) = multi_progress { + if let Ok(total_size) = disk_usage(&[from], true) { let bar = ProgressBar::new(total_size).with_style( ProgressStyle::with_template( "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", ) .unwrap(), ); + progress_bar = Some(multi_progress.add(bar)); + } + } - Some(multi_progress.add(bar)) - } else { - None - }; - - #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - let xattrs = - fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| std::collections::HashMap::new()); - - let result = if let Some(ref pb) = progress_bar { - move_dir_with_progress(from, to, &options, |process_info: TransitProcess| { - pb.set_position(process_info.copied_bytes); - pb.set_message(process_info.file_name); - TransitProcessResult::ContinueOrAbort - }) - } else { - move_dir(from, to, &options) - }; - - #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] - fsxattr::apply_xattrs(to, xattrs).unwrap(); + let result = move_dir(from, to, progress_bar.as_ref(), verbose_context); if let Err(err) = result { - return match err.kind { - fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new( + return match err { + MvError::FsXError(fs_extra::error::Error { + kind: fs_extra::error::ErrorKind::PermissionDenied, + .. + }) => Err(io::Error::new( io::ErrorKind::PermissionDenied, "Permission denied", )), + MvError::NotAllFilesMoved => { + Err(io::Error::new(io::ErrorKind::Other, String::new())) + } _ => Err(io::Error::new(io::ErrorKind::Other, format!("{err:?}"))), }; } @@ -701,11 +768,27 @@ fn rename_with_fallback( } #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] fs::copy(from, to) - .and_then(|_| fsxattr::copy_xattrs(&from, &to)) - .and_then(|_| fs::remove_file(from))?; + .map(|v| { + if let Some(vc) = verbose_context { + vc.print_copy_file(from, to, true); + } + v + }) + .and_then(|_| fsxattr::copy_xattrs(&from, &to))?; #[cfg(any(target_os = "macos", target_os = "redox", not(unix)))] - fs::copy(from, to).and_then(|_| fs::remove_file(from))?; + fs::copy(from, to).map(|v| { + if let Some(vc) = verbose_context { + vc.print_copy_file(from, to, true); + } + v + })?; + fs::remove_file(from)?; + if let Some(vc) = verbose_context { + vc.remove_file(from); + } } + } else if let Some(vb) = verbose_context { + vb.print_move_file(from, to); } Ok(()) } @@ -751,3 +834,505 @@ fn is_empty_dir(path: &Path) -> bool { Err(_e) => false, } } + +/// Moves a directory from one location to another with progress tracking. +/// This function assumes that `from` is a directory and `to` does not exist. +/// +/// Returns: +/// - `Result`: The total number of bytes moved if successful. +fn move_dir( + from: &Path, + to: &Path, + progress_bar: Option<&ProgressBar>, + verbose_context: Option<&VerboseContext<'_>>, +) -> MvResult { + // The return value that represents the number of bytes copied. + let mut result: u64 = 0; + let mut error_occurred = false; + let mut moved_entries: Vec<(PathBuf, fs::FileType, PathBuf, Option, usize)> = + vec![]; + for dir_entry_result in WalkDir::new(from) { + match dir_entry_result { + Ok(dir_entry) => { + let file_type = dir_entry.file_type(); + let dir_entry_md = dir_entry.metadata().ok(); + let depth = dir_entry.depth(); + let dir_entry_path = dir_entry.into_path(); + let tmp_to = dir_entry_path.strip_prefix(from).unwrap(); + let dir_entry_to = to.join(tmp_to); + if file_type.is_dir() { + let res = fs_extra::dir::create(&dir_entry_to, false); + if let Err(err) = res { + if let FsXErrorKind::NotFound = err.kind { + // This error would be thrown in the first iteration + // if the destination parent directory doesn't + // exist. + return Err(err.into()); + } + error_occurred = true; + show_error!("{:?}", err); + continue; + } + if let Some(vc) = verbose_context { + vc.create_directory(&dir_entry_to); + } + } else { + let res = copy_file(&dir_entry_path, &dir_entry_to, progress_bar, result); + match res { + Ok(copied_bytes) => { + result += copied_bytes; + if let Some(vc) = verbose_context { + vc.print_copy_file(&dir_entry_path, &dir_entry_to, false); + } + } + Err(err) => { + let err_msg = match err.kind { + FsXErrorKind::Io(error) => { + format!("error writing {}: {}", dir_entry_to.quote(), error) + } + _ => { + format!("{:?}", err) + } + }; + show_error!("{}", err_msg); + error_occurred = true; + continue; + } + } + } + moved_entries.push((dir_entry_path, file_type, dir_entry_to, dir_entry_md, depth)); + } + Err(err) => { + let err_msg = match (err.io_error(), err.path()) { + (Some(io_error), Some(path)) => { + format!("cannot access {}: {io_error}", path.quote()) + } + _ => err.to_string(), + }; + show_error!("{err_msg}"); + error_occurred = true; + } + } + } + // if no error occurred try to remove source and copy metadata + if !error_occurred { + // GNU's `mv` only reports an error when it fails to remove a directory + // entry. It doesn't print anything if it fails to remove the parent + // directory of that entry. + // in order to mimic that behavior, we need to remember where the last error occurred. + let mut last_rem_err_depth: Option = None; + while let Some((src_path, file_type, dest_path, src_md, depth)) = moved_entries.pop() { + if let Some(src_metadata) = src_md { + copy_metadata(&src_path, &dest_path, &src_metadata); + } + if matches!(last_rem_err_depth,Some(lred)if lred > depth) { + // This means current dir entry is parent directory of a child + // dir entry that couldn't be removed. + + // We mark current depth as the depth last error was occurred, this + // would ensure that we won't ignore sibling dir entries of the + // parent directory. + last_rem_err_depth = Some(depth); + // there's no point trying to remove a non empty directory. + continue; + } + let res = if src_path.is_dir() { + fs::remove_dir(&src_path) + } else { + fs::remove_file(&src_path) + }; + if let Err(err) = res { + error_occurred = true; + show_error!("cannot remove {}: {}", src_path.quote(), err); + last_rem_err_depth = Some(depth); + } else if let Some(vc) = verbose_context { + if file_type.is_dir() { + vc.remove_directory(&src_path); + } else { + vc.remove_file(&src_path); + } + } + } + } + if error_occurred { + return Err(MvError::NotAllFilesMoved); + } + Ok(result) +} + +/// Copies a file from one path to another, updating the progress bar if provided. +fn copy_file( + from: &Path, + to: &Path, + progress_bar: Option<&ProgressBar>, + progress_bar_start_val: u64, +) -> FsXResult { + let copy_options: CopyOptions = CopyOptions { + // We are overwriting here based on the assumption that the update and + // override options are handled by a parent function call. + overwrite: true, + ..Default::default() + }; + let progress_handler = if let Some(progress_bar) = progress_bar { + let display_file_name = from + .file_name() + .and_then(|file_name| file_name.to_str()) + .map(|file_name| file_name.to_string()) + .unwrap_or_default(); + let progress_handler = |info: file::TransitProcess| { + let copied_bytes = progress_bar_start_val + info.copied_bytes; + progress_bar.set_position(copied_bytes); + }; + progress_bar.set_message(display_file_name); + Some(progress_handler) + } else { + None + }; + + #[cfg(all(unix, not(target_os = "redox")))] + { + let md = from.metadata()?; + if FileTypeExt::is_fifo(&md.file_type()) { + let file_size = md.len(); + uucore::fs::create_fifo(to)?; + if let Some(progress_bar) = progress_bar { + progress_bar.set_position(file_size + progress_bar_start_val); + } + return Ok(file_size); + } + } + if let Some(progress_handler) = progress_handler { + file::copy_with_progress(from, to, ©_options, progress_handler) + } else { + file::copy(from, to, ©_options) + } +} + +// For GNU compatibility, this function does not report any errors that occur within it. +#[allow(unused_variables)] +fn copy_metadata(src: &Path, dest: &Path, src_metadata: &fs::Metadata) { + // Copy file permissions + let permissions = src_metadata.permissions(); + fs::set_permissions(dest, permissions).ok(); + + // Copy ownership (if on Unix-like system) + #[cfg(unix)] + { + let uid = MetadataExt::uid(src_metadata); + let gid = MetadataExt::gid(src_metadata); + if let Ok(dest_md) = fs::symlink_metadata(dest).as_ref() { + wrap_chown( + dest, + dest_md, + Some(uid), + Some(gid), + false, + Verbosity { + groups_only: false, + level: VerbosityLevel::Silent, + }, + ) + .ok(); + } + } + + // Copy the modified and accessed timestamps + let modified_time = src_metadata.modified(); + let accessed_time = src_metadata.accessed(); + if let (Ok(modified_time), Ok(accessed_time)) = (modified_time, accessed_time) { + set_symlink_file_times(dest, accessed_time.into(), modified_time.into()).ok(); + } + + // Copy xattrs. + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + fsxattr::copy_xattrs(src, dest).ok(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::copy_file; + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + use fsxattr::{apply_xattrs, retrieve_xattrs}; + use indicatif::ProgressBar; + use std::fs::{self, create_dir_all, File}; + use std::io::Write; + #[cfg(unix)] + use std::os::unix::fs::{FileTypeExt, PermissionsExt}; + use std::thread::sleep; + use std::time::Duration; + use tempfile::tempdir; + #[cfg(unix)] + use uucore::fs::create_fifo; + use uucore::fs::disk_usage; + + #[test] + fn move_all_files_and_directories() { + let tempdir = tempdir().expect("couldn't create tempdir"); + let tempdir_path = tempdir.path(); + let mut from = tempdir_path.to_path_buf(); + from.push("test_src"); + let mut to = tempdir_path.to_path_buf(); + to.push("test_dest"); + + // Setup source directory with files and subdirectories + create_dir_all(from.join("subdir")).expect("couldn't create subdir"); + let mut file = File::create(from.join("file1.txt")).expect("couldn't create file1.txt"); + writeln!(file, "Hello, world!").expect("couldn't write to file1.txt"); + let mut file = + File::create(from.join("subdir/file2.txt")).expect("couldn't create subdir/file2.txt"); + writeln!(file, "Hello, subdir!").expect("couldn't write to subdir/file2.txt"); + + // Call the function + let result: MvResult = move_dir(&from, &to, None, None); + + // Assert the result + assert!(result.is_ok()); + assert!(to.join("file1.txt").exists()); + assert!(to.join("subdir/file2.txt").exists()); + assert!(!from.join("file1.txt").exists()); + assert!(!from.join("subdir/file2.txt").exists()); + assert!(!from.exists()); + } + + #[test] + fn move_dir_tracks_progress() { + // Create a temporary directory for testing + let tempdir = tempdir().expect("couldn't create tempdir"); + let tempdir_path = tempdir.path(); + let mut from = tempdir_path.to_path_buf(); + from.push("test_src"); + let mut to = tempdir_path.to_path_buf(); + to.push("test_dest"); + + // Setup source directory with files and subdirectories + create_dir_all(from.join("subdir")).expect("couldn't create subdir"); + { + let mut file = File::create(from.join("file1.txt")).expect("couldn't create file1.txt"); + writeln!(file, "Hello, world!").expect("couldn't write to file1.txt"); + file.sync_all().unwrap(); + } + { + let mut file = File::create(from.join("subdir/file2.txt")) + .expect("couldn't create subdir/file2.txt"); + writeln!(file, "Hello, subdir!").expect("couldn't write to subdir/file2.txt"); + file.sync_all().unwrap(); + } + + let len = disk_usage(&[&from], true).expect("couldn't get the size of source dir"); + let pb = ProgressBar::new(len); + + // Call the function + let result: MvResult = move_dir(&from, &to, Some(&pb), None); + + // Assert the result + assert!(result.is_ok()); + assert!(to.join("file1.txt").exists()); + assert!(to.join("subdir/file2.txt").exists()); + assert!(!from.join("file1.txt").exists()); + assert!(!from.join("subdir/file2.txt").exists()); + assert!(!from.exists()); + assert_eq!(pb.position(), len) + } + + #[cfg(unix)] + #[test] + fn move_all_files_and_directories_without_src_permission() { + let tempdir = tempdir().expect("couldn't create tempdir"); + let tempdir_path = tempdir.path(); + let mut from = tempdir_path.to_path_buf(); + from.push("test_src"); + let mut to = tempdir_path.to_path_buf(); + to.push("test_dest"); + + // Setup source directory with files and subdirectories + create_dir_all(from.join("subdir")).expect("couldn't create subdir"); + + let mut file = File::create(from.join("file1.txt")).expect("couldn't create file1.txt"); + writeln!(file, "Hello, world!").expect("couldn't write to file1.txt"); + let mut file = + File::create(from.join("subdir/file2.txt")).expect("couldn't create subdir/file2.txt"); + writeln!(file, "Hello, subdir!").expect("couldn't write to subdir/file2.txt"); + + let metadata = fs::metadata(&from).expect("failed to get metadata"); + let mut permissions = metadata.permissions(); + std::os::unix::fs::PermissionsExt::set_mode(&mut permissions, 0o222); + fs::set_permissions(&from, permissions).expect("failed to set permissions"); + + // Call the function + let result: MvResult = move_dir(&from, &to, None, None); + assert!(matches!(result, Err(MvError::NotAllFilesMoved))); + assert!(from.exists()); + } + + #[test] + fn test_copy_file() { + let temp_dir = tempdir().expect("couldn't create tempdir"); + let from = temp_dir.path().join("test_source.txt"); + let to = temp_dir.path().join("test_destination.txt"); + + // Create a test source file + let mut file = File::create(&from).expect("couldn't create file1.txt"); + write!(file, "Hello, world!").expect("couldn't write to file1.txt"); + + // Call the function + let result = copy_file(&from, &to, None, 0); + + // Assert the result is Ok and the file was copied + assert!(result.is_ok()); + assert!(to.exists()); + assert_eq!( + fs::read_to_string(to).expect("couldn't read from to"), + "Hello, world!" + ); + } + #[test] + fn test_copy_file_with_progress() { + let temp_dir = tempdir().expect("couldn't create tempdir"); + let from = temp_dir.path().join("test_source.txt"); + let to = temp_dir.path().join("test_destination.txt"); + + // Create a test source file + let mut file = File::create(&from).expect("couldn't create file1.txt"); + write!(file, "Hello, world!").expect("couldn't write to file1.txt"); + + let len = file + .metadata() + .expect("couldn't get source file metadata") + .len(); + let pb = ProgressBar::new(len); + + // Call the function + let result = copy_file(&from, &to, Some(&pb), 0); + + // Assert the result is Ok and the file was copied + assert_eq!(pb.position(), len); + assert!(result.is_ok()); + assert!(to.exists()); + assert_eq!( + fs::read_to_string(to).expect("couldn't read from to"), + "Hello, world!" + ); + } + + #[cfg(all(unix, not(target_os = "redox")))] + #[test] + fn test_copy_file_with_fifo() { + let temp_dir = tempdir().expect("couldn't create tempdir"); + let from = temp_dir.path().join("test_source.txt"); + let to = temp_dir.path().join("test_destination.txt"); + + // Create a test source file + create_fifo(&from).expect("couldn't create fifo"); + + // Call the function + let result = copy_file(&from, &to, None, 0); + + // Assert the result is Ok and the fifo was copied + assert!(result.is_ok()); + assert!(to.exists()); + assert!(to + .metadata() + .expect("couldn't get metadata") + .file_type() + .is_fifo()) + } + + #[cfg(unix)] + #[test] + fn test_copy_metadata_copies_permissions() { + let temp_dir = tempdir().unwrap(); + let src_path = temp_dir.path().join("src_file"); + let dest_path = temp_dir.path().join("dest_file"); + + // Create source and destination files + File::create(&src_path).unwrap(); + File::create(&dest_path).unwrap(); + + // Set permissions for the source file + let src_md = fs::metadata(&src_path).unwrap(); + let mut permissions = src_md.permissions(); + permissions.set_mode(0o100000); + fs::set_permissions(&src_path, permissions.clone()).unwrap(); + let src_md = fs::metadata(&src_path).unwrap(); + + // Call the function under test + copy_metadata(&src_path, &dest_path, &src_md); + + // Verify that the permissions were copied + let dest_permissions = fs::metadata(&dest_path).unwrap().permissions(); + assert_eq!(permissions.mode(), dest_permissions.mode()); + } + + #[test] + fn test_copy_metadata_copies_file_times() { + let temp_dir = tempdir().expect("couldn't create tempdir"); + let src_path = temp_dir.path().join("src_file"); + let dest_path = temp_dir.path().join("dest_file"); + + // Create source and destination files + File::create(&src_path).expect("couldn't create source file"); + // Wait for a second so that file times are different + sleep(Duration::from_secs(1)); + File::create(&dest_path).expect("couldn't create dest file"); + + // Get file times for the source file + let src_metadata = fs::metadata(&src_path).expect("couldn't get metadata for source file"); + let modified_time = src_metadata + .modified() + .expect("couldn't get modified time for src file"); + let accessed_time = src_metadata + .accessed() + .expect("couldn't get accessed time for src file"); + + //Try to copy metadata + copy_metadata(&src_path, &dest_path, &src_metadata); + + // Get file times for the dest file + let dest_metadata = fs::metadata(&dest_path).expect("couldn't get metadata for dest file"); + let dest_modified_time = dest_metadata + .modified() + .expect("couldn't get modified time for src file"); + let dest_accessed_time = dest_metadata + .accessed() + .expect("couldn't get accessed time for src file"); + + // Verify that the file times were copied + assert_eq!(modified_time, dest_modified_time); + assert_eq!(dest_accessed_time, accessed_time); + } + + #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] + #[test] + fn test_copy_metadata_copies_xattr() { + let temp_dir = tempdir().expect("couldn't create tempdir"); + let src_path = temp_dir.path().join("src_file"); + let dest_path = temp_dir.path().join("dest_file"); + + // Create source and destination files + File::create(&src_path).expect("couldn't create source file"); + File::create(&dest_path).expect("couldn't create dest file"); + + let src_metadata = fs::metadata(&src_path).unwrap(); + + // Set xattrs for the source file + let mut test_xattrs = std::collections::HashMap::new(); + let test_attr = "user.test_attr"; + let test_value = b"test value"; + test_xattrs.insert(OsString::from(test_attr), test_value.to_vec()); + apply_xattrs(&src_path, test_xattrs).expect("couldn't apply xattr to the destination file"); + + //Try to copy metadata + copy_metadata(&src_path, &dest_path, &src_metadata); + + // Verify that the xattrs were copied + let retrieved_xattrs = retrieve_xattrs(&dest_path).unwrap(); + assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str())); + assert_eq!( + retrieved_xattrs + .get(OsString::from(test_attr).as_os_str()) + .expect("couldn't find xattr with name user.test_attr"), + test_value + ); + } +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index e0c8ea79d3a..7064c7c6f76 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -789,6 +789,49 @@ pub fn get_filename(file: &Path) -> Option<&str> { file.file_name().and_then(|filename| filename.to_str()) } +// "Copies" a FIFO by creating a new one. This workaround is because Rust's +// built-in fs::copy does not handle FIFOs (see rust-lang/rust/issues/79390). +#[cfg(all(unix, not(target_os = "redox")))] +pub fn create_fifo(dest: &Path) -> std::io::Result<()> { + nix::unistd::mkfifo(dest, nix::sys::stat::Mode::S_IRUSR).map_err(|err| err.into()) +} + +/// Get the total size of a slice of files and directories. +/// +/// This function is much like the `du` utility, by recursively getting the sizes of files in directories. +/// Files are not deduplicated when appearing in multiple sources. If `recursive` is set to `false`, the +/// directories in `paths` will be ignored. +pub fn disk_usage>(paths: &[P], recursive: bool) -> IOResult { + let mut total = 0; + for p in paths { + let md = fs::metadata(p)?; + if md.file_type().is_dir() { + if recursive { + total += disk_usage_directory(p)?; + } + } else { + total += md.len(); + } + } + Ok(total) +} + +/// A helper for `disk_usage` specialized for directories. +fn disk_usage_directory>(p: P) -> IOResult { + let mut total = 0; + + for entry in fs::read_dir(p)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + total += disk_usage_directory(entry.path())?; + } else { + total += entry.metadata()?.len(); + } + } + + Ok(total) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. @@ -1030,4 +1073,87 @@ mod tests { let file_path = PathBuf::from("~/foo.txt"); assert!(matches!(get_filename(&file_path), Some("foo.txt"))); } + + #[cfg(all(unix, not(target_os = "redox")))] + #[test] + fn test_create_fifo_success() { + let dir = tempdir().unwrap(); + let fifo_path = dir.path().join("test_fifo"); + + // Call the function to create a FIFO + create_fifo(&fifo_path).unwrap(); + + // Check if the FIFO was created successfully + let metadata = fs::metadata(&fifo_path).unwrap(); + assert!(unix::fs::FileTypeExt::is_fifo(&metadata.file_type())); + } + + #[test] + fn calculates_total_size_of_single_file() { + let dir = tempdir().unwrap(); + let file_path1 = dir.path().join("file1.txt"); + + let mut file1 = fs::File::create(&file_path1).unwrap(); + writeln!(file1, "Hello, world!").unwrap(); + + let paths = vec![file_path1]; + + let actual_len = file1 + .metadata() + .expect("couldn't get metadata for file1") + .len(); + + let total_size = disk_usage(&paths, false).unwrap(); + + assert_eq!(total_size, actual_len); + } + #[test] + fn calculates_total_size_of_files() { + let dir = tempdir().unwrap(); + let file_path1 = dir.path().join("file1.txt"); + let file_path2 = dir.path().join("file2.txt"); + + let mut file1 = fs::File::create(&file_path1).unwrap(); + writeln!(file1, "Hello, world!").unwrap(); + + let mut file2 = fs::File::create(&file_path2).unwrap(); + writeln!(file2, "Rust programming!").unwrap(); + + let paths = vec![file_path1, file_path2]; + + let file_1_len = file1 + .metadata() + .expect("couldn't get metadata for file1") + .len(); + let file_2_len = file2 + .metadata() + .expect("couldn't get metadata for file1") + .len(); + let actual_len = file_1_len + file_2_len; + + let total_size = disk_usage(&paths, false).unwrap(); + + assert_eq!(total_size, actual_len); + } + #[test] + fn test_disk_usage_recursive() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("file1.txt"); + let mut file = fs::File::create(&file_path).unwrap(); + writeln!(file, "Hello, world!").unwrap(); + + let subdir_path = dir.path().join("subdir"); + fs::create_dir(&subdir_path).unwrap(); + let sub_file_path = subdir_path.join("file2.txt"); + let mut sub_file = fs::File::create(&sub_file_path).unwrap(); + writeln!(sub_file, "Hello, again!").unwrap(); + + let paths: Vec = vec![dir.path().to_path_buf()]; + let total_size = disk_usage(&paths, true).unwrap(); + + assert_eq!( + total_size, + file_path.metadata().unwrap().len() + sub_file_path.metadata().unwrap().len() + ); + } } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index daab2200956..4f87061f809 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1626,10 +1626,50 @@ fn test_acl() { #[cfg(target_os = "linux")] mod inter_partition_copying { + use super::*; use crate::common::util::TestScenario; - use std::fs::{read_to_string, set_permissions, write}; - use std::os::unix::fs::{symlink, PermissionsExt}; + use std::ffi::OsString; + use std::fs::{read_to_string, set_permissions, write, File}; + use std::os::unix::fs::{symlink, FileTypeExt, MetadataExt, PermissionsExt}; use tempfile::TempDir; + use uucore::display::Quotable; + use uucore::fsxattr::retrieve_xattrs; + use xattr::FileExt; + // Ensure that the copying code used in an inter-partition move preserve dir structure. + #[test] + fn test_inter_partition_copying_folder() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/b/c"); + at.write("a/b/d", "d"); + at.write("a/b/c/e", "e"); + + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // mv to other partition + scene + .ucmd() + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds(); + + // make sure that a got removed. + assert!(!at.dir_exists("a")); + + // Ensure that the folder structure is preserved, files are copied, and their contents remain intact. + assert_eq!( + read_to_string(other_fs_tempdir.path().join("a/b/d"),) + .expect("Unable to read other_fs_file"), + "d" + ); + assert_eq!( + read_to_string(other_fs_tempdir.path().join("a/b/c/e"),) + .expect("Unable to read other_fs_file"), + "e" + ); + } // Ensure that the copying code used in an inter-partition move unlinks the destination symlink. #[test] @@ -1716,4 +1756,466 @@ mod inter_partition_copying { .stderr_contains("inter-device move failed:") .stderr_contains("Permission denied"); } + + #[cfg(unix)] + #[test] + fn test_inter_partition_copying_folder_with_fifo() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/b/c"); + at.write("a/b/d", "d"); + at.write("a/b/c/e", "e"); + at.mkfifo("a/b/c/f"); + + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // mv to other partition + scene + .ucmd() + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds(); + + // make sure that a got removed. + assert!(!at.dir_exists("a")); + + // Ensure that the folder structure is preserved, files are copied, and their contents remain intact. + assert_eq!( + read_to_string(other_fs_tempdir.path().join("a/b/d"),) + .expect("Unable to read other_fs_file"), + "d" + ); + assert_eq!( + read_to_string(other_fs_tempdir.path().join("a/b/c/e"),) + .expect("Unable to read other_fs_file"), + "e" + ); + assert!(other_fs_tempdir + .path() + .join("a/b/c/f") + .metadata() + .expect("") + .file_type() + .is_fifo()); + } + + #[test] + fn test_inter_partition_copying_folder_with_verbose() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/b/c"); + at.write("a/b/d", "d"); + at.write("a/b/c/e", "e"); + + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds() + .stdout_contains(format!( + "created directory {}", + other_fs_tempdir.path().join("a").to_string_lossy().quote() + )) + .stdout_contains(format!( + "created directory {}", + other_fs_tempdir + .path() + .join("a/b/c") + .to_string_lossy() + .quote() + )) + .stdout_contains(format!( + "copied 'a/b/d' -> {}", + other_fs_tempdir + .path() + .join("a/b/d") + .to_string_lossy() + .quote() + )) + .stdout_contains("removed 'a/b/c/e'") + .stdout_contains("removed directory 'a/b'"); + } + #[test] + fn test_inter_partition_copying_file_with_verbose() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("a", "file_contents"); + + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds() + .stdout_contains(format!( + "copied 'a' -> {}", + other_fs_tempdir.path().join("a").to_string_lossy().quote() + )) + .stdout_contains("removed 'a'"); + + at.write("a", "file_contents"); + + // mv to other partition + scene + .ucmd() + .arg("-vb") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds() + .stdout_contains(format!( + "copied 'a' -> {} (backup: {})", + other_fs_tempdir.path().join("a").to_string_lossy().quote(), + other_fs_tempdir.path().join("a~").to_string_lossy().quote() + )) + .stdout_contains("removed 'a'"); + } + + #[test] + fn test_inter_partition_copying_dir_without_read_permission_to_sub_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/b/c"); + at.write("a/b/d", "d"); + at.write("a/b/c/e", "e"); + at.mkdir_all("a/b/f"); + at.write("a/b/f/g", "g"); + at.write("a/b/h", "h"); + at.set_mode("a/b/f", 0); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .fails() + // check error occurred + .stderr_contains("cannot access 'a/b/f': Permission denied"); + + // make sure mv kept on going after error + assert_eq!( + read_to_string(other_fs_tempdir.path().join("a/b/h"),) + .expect("Unable to read other_fs_file"), + "h" + ); + + // make sure mv didn't remove src because an error occurred + at.dir_exists("a"); + at.file_exists("a/b/h"); + } + + #[test] + fn test_inter_partition_copying_directory_metadata_for_file() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("dir"); + let file = at.make_file("dir/file"); + at.set_mode("dir/file", 0o100_700); + + // Set xattrs for the source file + let test_attr = "user.test_attr"; + let test_value = b"test value"; + file.set_xattr(test_attr, test_value) + .expect("couldn't set xattr for src file"); + + // Get file times for the source file + let src_metadata = file + .metadata() + .expect("couldn't get metadata for source file"); + let modified_time = src_metadata + .modified() + .expect("couldn't get modified time for src file"); + let accessed_time = src_metadata + .accessed() + .expect("couldn't get accessed time for src file"); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + + // https://github.com/Stebalien/xattr/issues/24#issuecomment-1279682009 + let mut dest_fs_xattr_support = true; + let tempfile_path = other_fs_tempdir.path().join("temp_file"); + let tf = File::create(&tempfile_path).expect("couldn't create tempfile"); + if let Err(err) = tf.set_xattr(test_attr, test_value) { + dest_fs_xattr_support = false; + println!("no fs xattr support: {err}"); + } + + // make sure to wait for a second so that when the dest file is created, it + // would have a different filetime so that the only way the dest file + // would have a same timestamp is by copying the timestamp of src file. + sleep(Duration::from_secs(1)); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("dir") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds(); + + let dest_metadata = other_fs_tempdir + .path() + .join("dir/file") + .metadata() + .expect("couldn't get metadata of dest file"); + let mode = dest_metadata.mode(); + assert_eq!(mode, 0o100_700, "permission doesn't match"); + assert_eq!( + dest_metadata + .modified() + .expect("couldn't get modified time for dest file"), + modified_time + ); + assert_eq!( + dest_metadata + .accessed() + .expect("couldn't get accessed time for dest file"), + accessed_time + ); + + if dest_fs_xattr_support { + // Verify that the xattrs were copied + let retrieved_xattrs = + retrieve_xattrs(other_fs_tempdir.path().join("dir/file")).unwrap(); + assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str())); + assert_eq!( + retrieved_xattrs + .get(OsString::from(test_attr).as_os_str()) + .expect("couldn't find xattr with name user.test_attr"), + test_value + ); + } + } + + // this test would fail if progress bar flag is set to true because we need + // to traverse directory in order to find the size that would change the + // access time. + #[test] + fn test_inter_partition_copying_directory_metadata_for_directory() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("dir"); + at.make_file("dir/file"); + at.set_mode("dir", 0o40700); + + // Set xattrs for the source file + let test_attr = "user.test_attr"; + let test_value = b"test value"; + xattr::set(at.plus("dir"), test_attr, test_value).expect("couldn't set xattr for src file"); + + // Get file times for the source dir + let src_metadata = at + .plus("dir") + .metadata() + .expect("couldn't get metadata for source file"); + let modified_time = src_metadata + .modified() + .expect("couldn't get modified time for src file"); + let accessed_time = src_metadata + .accessed() + .expect("couldn't get accessed time for src file"); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + // make sure to wait for a second so that when the dest file is created, it + // would have a different filetime so that the only way the dest file + // would have a same timestamp is by copying the timestamp of src file. + + // https://github.com/Stebalien/xattr/issues/24#issuecomment-1279682009 + let mut dest_fs_xattr_support = true; + let tempfile_path = other_fs_tempdir.path().join("temp_file"); + let tf = File::create(&tempfile_path).expect("couldn't create tempfile"); + if let Err(err) = tf.set_xattr(test_attr, test_value) { + dest_fs_xattr_support = false; + println!("no fs xattr support: {err}"); + } + + sleep(Duration::from_secs(1)); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("dir") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .succeeds(); + + let dest_metadata = other_fs_tempdir + .path() + .join("dir") + .metadata() + .expect("couldn't get metadata of dest file"); + let mode = dest_metadata.mode(); + assert_eq!(mode, 0o40700, "permission doesn't match"); + assert_eq!( + dest_metadata + .modified() + .expect("couldn't get modified time for dest file"), + modified_time + ); + assert_eq!( + dest_metadata + .accessed() + .expect("couldn't get accessed time for dest file"), + accessed_time + ); + + if dest_fs_xattr_support { + // Verify that the xattrs were copied + let retrieved_xattrs = retrieve_xattrs(other_fs_tempdir.path().join("dir")).unwrap(); + assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str())); + assert_eq!( + retrieved_xattrs + .get(OsString::from(test_attr).as_os_str()) + .expect("couldn't find xattr with name user.test_attr"), + test_value + ); + } + } + + #[test] + fn test_inter_partition_removing_source_without_permission() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/aa/"); + at.write("a/aa/aa1", "file contents"); + //remove write permission for the subdir + at.set_mode("a/aa/", 0o555); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .fails() + // check error occurred + .stderr_contains("mv: cannot remove 'a/aa/aa1': Permission denied") + //make sure mv doesn't print errors for the parent directories + .stderr_does_not_contain("'a/aa'"); + assert!(at.file_exists("a/aa/aa1")); + assert!(other_fs_tempdir.path().join("a/aa/aa1").exists()); + } + + #[test] + fn test_inter_partition_removing_source_without_permission_2() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/aa/"); + at.mkdir_all("a/aa/aaa"); + at.write("a/aa/aa1", "file contents"); + at.write("a/aa/aa2", "file contents"); + //remove write permission for the subdir + at.set_mode("a/aa/", 0o555); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .fails() + // make sure mv prints error message for each entry where error occurs + .stderr_contains("mv: cannot remove 'a/aa/aaa': Permission denied") + .stderr_contains("mv: cannot remove 'a/aa/aa1': Permission denied") + .stderr_contains("mv: cannot remove 'a/aa/aa2': Permission denied"); + assert!(at.file_exists("a/aa/aa1")); + assert!(at.file_exists("a/aa/aa2")); + assert!(at.dir_exists("a/aa/aaa")); + assert!(other_fs_tempdir.path().join("a/aa/aa1").exists()); + } + + #[test] + fn test_inter_partition_removing_source_without_permission_3() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/aa/"); + at.write("a/a1", "file contents"); + at.write("a/aa/aa1", "file contents"); + //remove write permission for the subdir + at.set_mode("a/aa/", 0o555); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .fails() + .stderr_contains("mv: cannot remove 'a/aa/aa1': Permission denied"); + assert!(at.file_exists("a/aa/aa1")); + // file that doesn't belong to the branch that error occurred didn't got removed + assert!(!at.file_exists("a/a1")); + assert!(other_fs_tempdir.path().join("a/aa/aa1").exists()); + } + + #[test] + fn test_inter_partition_removing_source_without_permission_4() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("a/aa/"); + at.mkdir_all("a/ab/"); + at.write("a/aa/aa1", "file contents"); + at.write("a/ab/ab1", "file contents"); + + //remove write permission for the subdir + at.set_mode("a/aa/", 0o555); + // create a folder in another partition. + let other_fs_tempdir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory"); + // mv to other partition + scene + .ucmd() + .arg("-v") + .arg("a") + .arg(other_fs_tempdir.path().to_str().unwrap()) + .fails() + .stderr_contains("mv: cannot remove 'a/aa/aa1': Permission denied"); + assert!(at.file_exists("a/aa/aa1")); + // folder that doesn't belong to the branch that error occurred didn't got removed + assert!(!at.dir_exists("a/ab")); + assert!(other_fs_tempdir.path().join("a/ab/ab1").exists()); + } +} + +#[cfg(unix)] +#[test] +fn test_mv_backup_when_dest_has_trailing_slash() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("D"); + at.mkdir("E"); + at.touch("D/d_file"); + at.touch("E/e_file"); + scene + .ucmd() + .arg("-T") + .arg("--backup=numbered") + .arg("D") + .arg("E/") + .succeeds(); + assert!(at.dir_exists("E.~1~"), "Backup folder not created"); + assert!(at.file_exists("E.~1~/e_file"), "Backup file not created"); + assert!(at.file_exists("E/d_file"), "source file didn't move"); }