From 119ce648e3ec33342d7544026971d6de8f6a11e1 Mon Sep 17 00:00:00 2001 From: mhead Date: Wed, 11 Sep 2024 13:13:46 +0530 Subject: [PATCH] mv: copy dir --- Cargo.lock | 2 + src/uu/mv/Cargo.toml | 4 + src/uu/mv/src/mv.rs | 334 ++++++++++++++++++++++++++++-- src/uucore/src/lib/features/fs.rs | 7 + tests/by-util/test_mv.rs | 62 ++++++ 5 files changed, 390 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abaf5c3106f..3692a68d1d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2956,7 +2956,9 @@ dependencies = [ "clap", "fs_extra", "indicatif", + "tempfile", "uucore", + "walkdir", ] [[package]] diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index c484d5a77f3..8ee03bbfd06 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -20,6 +20,7 @@ path = "src/mv.rs" clap = { workspace = true } fs_extra = { workspace = true } indicatif = { workspace = true } +walkdir = { workspace = true } uucore = { workspace = true, features = [ "backup-control", "fs", @@ -27,6 +28,9 @@ uucore = { workspace = true, features = [ "update-control", ] } +[dev-dependencies] +tempfile = { workspace = true } + [[bin]] name = "mv" path = "src/main.rs" diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 7edecb960fc..3d05475f43b 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -20,6 +20,8 @@ use std::os::unix; #[cfg(windows)] use std::os::windows; use std::path::{Path, PathBuf}; +#[cfg(unix)] +use unix::fs::FileTypeExt; use uucore::backup_control::{self, source_is_target_backup}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UResult, USimpleError, UUsageError}; @@ -30,15 +32,17 @@ use uucore::fs::{ #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; use uucore::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::{ + dir::{create_all, get_size as dir_get_size, remove}, + error::Result as FsXResult, + file::{self, CopyOptions}, }; use crate::error::MvError; @@ -605,13 +609,6 @@ 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: @@ -638,15 +635,7 @@ fn rename_with_fallback( 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) - }; + let result = move_dir(from, to, progress_bar.as_ref()); #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] fsxattr::apply_xattrs(to, xattrs).unwrap(); @@ -726,3 +715,310 @@ 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>) -> FsXResult { + // The return value that represents the number of bytes copied. + let mut result: u64 = 0; + let mut error_occured = false; + for dir_entry_result in WalkDir::new(from) { + match dir_entry_result { + Ok(dir_entry) => { + if dir_entry.file_type().is_dir() { + let path = dir_entry.into_path(); + let tmp_to = path.strip_prefix(from)?; + let dir = to.join(tmp_to); + if !dir.exists() { + create_all(&dir, false)?; + } + } else { + let file = dir_entry.path(); + let tp = file.strip_prefix(from)?; + let to_file = to.join(tp); + let result_file_copy = copy_file(file, &to_file, progress_bar, result)?; + result += result_file_copy; + } + } + Err(_) => { + error_occured = true; + } + } + } + if !error_occured { + remove(from)?; + } + 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 + }; + let result_file_copy = { + let md = from.metadata()?; + if cfg!(unix) && FileTypeExt::is_fifo(&md.file_type()) { + let file_size = md.len(); + uucore::fs::copy_fifo(to)?; + if let Some(progress_bar) = progress_bar { + progress_bar.set_position(file_size + progress_bar_start_val); + } + Ok(file_size) + } else { + if let Some(progress_handler) = progress_handler { + file::copy_with_progress(from, to, ©_options, progress_handler) + } else { + file::copy(from, to, ©_options) + } + } + }; + result_file_copy +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + extern crate fs_extra; + use super::{copy_file, move_dir}; + use fs_extra::dir::*; + use indicatif::{ProgressBar, ProgressStyle}; + use tempfile::tempdir; + + // These tests are copied from the `fs_extra`'s repository + #[test] + fn it_move_work() { + for with_progress_bar in [false, true] { + let temp_dir = tempdir().unwrap(); + let mut path_from = PathBuf::from(temp_dir.path()); + let test_name = "sub"; + path_from.push("it_move_work"); + let mut path_to = path_from.clone(); + path_to.push("out"); + path_from.push(test_name); + + create_all(&path_from, true).unwrap(); + assert!(path_from.exists()); + create_all(&path_to, true).unwrap(); + assert!(path_to.exists()); + + let mut file1_path = path_from.clone(); + file1_path.push("test1.txt"); + let content1 = "content1"; + fs_extra::file::write_all(&file1_path, content1).unwrap(); + assert!(file1_path.exists()); + + let mut sub_dir_path = path_from.clone(); + sub_dir_path.push("sub"); + create(&sub_dir_path, true).unwrap(); + let mut file2_path = sub_dir_path.clone(); + file2_path.push("test2.txt"); + let content2 = "content2"; + fs_extra::file::write_all(&file2_path, content2).unwrap(); + assert!(file2_path.exists()); + + let pb = if with_progress_bar { + Some( + ProgressBar::new(16).with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ), + ) + } else { + None + }; + + let result = move_dir(&path_from, &path_to, pb.as_ref()).unwrap(); + + assert_eq!(16, result); + assert!(path_to.exists()); + assert!(!path_from.exists()); + if let Some(pb) = pb { + assert_eq!(pb.position(), 16); + } + } + } + + #[test] + fn it_move_exist_overwrite() { + for with_progress_bar in [false, true] { + let temp_dir = tempdir().unwrap(); + let mut path_from = PathBuf::from(temp_dir.path()); + let test_name = "sub"; + path_from.push("it_move_exist_overwrite"); + let mut path_to = path_from.clone(); + path_to.push("out"); + path_from.push(test_name); + let same_file = "test.txt"; + + create_all(&path_from, true).unwrap(); + assert!(path_from.exists()); + create_all(&path_to, true).unwrap(); + assert!(path_to.exists()); + + let mut file1_path = path_from.clone(); + file1_path.push(same_file); + let content1 = "content1"; + fs_extra::file::write_all(&file1_path, content1).unwrap(); + assert!(file1_path.exists()); + + let mut sub_dir_path = path_from.clone(); + sub_dir_path.push("sub"); + create(&sub_dir_path, true).unwrap(); + let mut file2_path = sub_dir_path.clone(); + file2_path.push("test2.txt"); + let content2 = "content2"; + fs_extra::file::write_all(&file2_path, content2).unwrap(); + assert!(file2_path.exists()); + + let mut exist_path = path_to.clone(); + exist_path.push(test_name); + create(&exist_path, true).unwrap(); + assert!(exist_path.exists()); + exist_path.push(same_file); + let exist_content = "exist content"; + assert_ne!(exist_content, content1); + fs_extra::file::write_all(&exist_path, exist_content).unwrap(); + assert!(exist_path.exists()); + + let dir_size = get_size(&path_from).expect("failed to get dir size"); + let pb = if with_progress_bar { + Some( + ProgressBar::new(dir_size).with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ), + ) + } else { + None + }; + move_dir(&path_from, &path_to, pb.as_ref()).unwrap(); + assert!(exist_path.exists()); + assert!(path_to.exists()); + assert!(!path_from.exists()); + if let Some(pb) = pb { + assert_eq!(pb.position(), dir_size); + } + } + } + + #[test] + fn it_move_inside_work_target_dir_not_exist() { + for with_progress_bar in [false, true] { + let temp_dir = tempdir().unwrap(); + let path_root = PathBuf::from(temp_dir.path()); + let root = path_root.join("it_move_inside_work_target_dir_not_exist"); + let root_dir1 = root.join("dir1"); + let root_dir1_sub = root_dir1.join("sub"); + let root_dir2 = root.join("dir2"); + let file1 = root_dir1.join("file1.txt"); + let file2 = root_dir1_sub.join("file2.txt"); + + create_all(&root_dir1_sub, true).unwrap(); + fs_extra::file::write_all(&file1, "content1").unwrap(); + fs_extra::file::write_all(&file2, "content2").unwrap(); + + if root_dir2.exists() { + remove(&root_dir2).unwrap(); + } + + assert!(root_dir1.exists()); + assert!(root_dir1_sub.exists()); + assert!(!root_dir2.exists()); + assert!(file1.exists()); + assert!(file2.exists()); + let dir_size = get_size(&root_dir1).expect("failed to get dir size"); + let pb = if with_progress_bar { + Some( + ProgressBar::new(dir_size).with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ), + ) + } else { + None + }; + + let result = move_dir(&root_dir1, &root_dir2, pb.as_ref()).unwrap(); + + assert_eq!(16, result); + assert!(!root_dir1.exists()); + let root_dir2_sub = root_dir2.join("sub"); + let root_dir2_file1 = root_dir2.join("file1.txt"); + let root_dir2_sub_file2 = root_dir2_sub.join("file2.txt"); + assert!(root_dir2.exists()); + assert!(root_dir2_sub.exists()); + assert!(root_dir2_file1.exists()); + assert!(root_dir2_sub_file2.exists()); + if let Some(pb) = pb { + assert_eq!(pb.position(), dir_size); + } + } + } + + #[test] + fn copy_file_test() { + for with_progress_bar in [false, true] { + let temp_dir = tempdir().unwrap(); + let temp_dir_path = temp_dir.path(); + + let file1_path = temp_dir_path.join("file"); + let content = "content"; + fs_extra::file::write_all(&file1_path, content).unwrap(); + assert!(file1_path.exists()); + let path_to = temp_dir_path.join("file_out"); + let pb = if with_progress_bar { + Some( + ProgressBar::new(7).with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ), + ) + } else { + None + }; + + let result = copy_file(&file1_path, &path_to, pb.as_ref(), 0).expect("move failed"); + + assert_eq!(7, result); + assert!(path_to.exists()); + if let Some(pb) = pb { + assert_eq!(pb.position(), 7); + } + } + } +} diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 73c61e0a3e4..4c7ac1c12a3 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -789,6 +789,13 @@ 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(unix)] +pub fn copy_fifo(dest: &Path) -> std::io::Result<()> { + nix::unistd::mkfifo(dest, nix::sys::stat::Mode::S_IRUSR).map_err(|err| err.into()) +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 82741d5f002..2bc91ac0af5 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1321,6 +1321,33 @@ fn test_mv_verbose() { "renamed '{file_a}' -> '{file_b}' (backup: '{file_b}~')\n" )); } +#[test] +fn test_verbose_src_symlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let file_a = "test_mv_verbose_file_a"; + let ln = "link"; + at.touch(file_a); + at.symlink_file(&file_a, &ln); + scene + .ucmd() + .arg("-v") + .arg(file_a) + .arg(ln) + .succeeds() + .stdout_only(format!("renamed '{file_a}' -> '{ln}'\n")); + + at.touch(file_a); + scene + .ucmd() + .arg("-vb") + .arg(file_a) + .arg(ln) + .succeeds() + .stdout_only(format!( + "renamed '{file_a}' -> '{ln}' (backup: '{ln}~')\n" + )); +} #[test] #[cfg(any(target_os = "linux", target_os = "android"))] // mkdir does not support -m on windows. Freebsd doesn't return a permission error either. @@ -1629,6 +1656,41 @@ mod inter_partition_copying { use std::fs::{read_to_string, set_permissions, write}; use std::os::unix::fs::{symlink, PermissionsExt}; use tempfile::TempDir; + // 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]