1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

feature(cp): implement archive + add missing tests

This commit is contained in:
Sylvestre Ledru 2020-08-04 23:15:33 +02:00
parent a10346eb23
commit f76a0ec972
2 changed files with 278 additions and 24 deletions

View file

@ -337,7 +337,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.arg(Arg::with_name(OPT_RECURSIVE) .arg(Arg::with_name(OPT_RECURSIVE)
.short("r") .short("r")
.long(OPT_RECURSIVE) .long(OPT_RECURSIVE)
.help("copy directories recursively")) // --archive sets this option
.help("copy directories recursively"))
.arg(Arg::with_name(OPT_RECURSIVE_ALIAS) .arg(Arg::with_name(OPT_RECURSIVE_ALIAS)
.short("R") .short("R")
.help("same as -r")) .help("same as -r"))
@ -395,7 +396,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.use_delimiter(true) .use_delimiter(true)
.possible_values(PRESERVABLE_ATTRIBUTES) .possible_values(PRESERVABLE_ATTRIBUTES)
.value_name("ATTR_LIST") .value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE]) .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE])
// -d sets this option
// --archive sets this option
.help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\ .help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\
if possible additional attributes: context, links, xattr, all")) if possible additional attributes: context, links, xattr, all"))
.arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES) .arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES)
@ -413,21 +416,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.short("-P") .short("-P")
.long(OPT_NO_DEREFERENCE) .long(OPT_NO_DEREFERENCE)
.conflicts_with(OPT_DEREFERENCE) .conflicts_with(OPT_DEREFERENCE)
// -d sets this option
.help("never follow symbolic links in SOURCE")) .help("never follow symbolic links in SOURCE"))
// TODO: implement the following args
.arg(Arg::with_name(OPT_ARCHIVE) .arg(Arg::with_name(OPT_ARCHIVE)
.short("a") .short("a")
.long(OPT_ARCHIVE) .long(OPT_ARCHIVE)
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE])
.help("NotImplemented: same as -dR --preserve=all")) .help("Same as -dR --preserve=all"))
.arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
.short("d")
.help("same as --no-dereference --preserve=links"))
// TODO: implement the following args
.arg(Arg::with_name(OPT_COPY_CONTENTS) .arg(Arg::with_name(OPT_COPY_CONTENTS)
.long(OPT_COPY_CONTENTS) .long(OPT_COPY_CONTENTS)
.conflicts_with(OPT_ATTRIBUTES_ONLY) .conflicts_with(OPT_ATTRIBUTES_ONLY)
.help("NotImplemented: copy contents of special files when recursive")) .help("NotImplemented: copy contents of special files when recursive"))
.arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
.short("d")
.help("NotImplemented: same as --no-dereference --preserve=links"))
.arg(Arg::with_name(OPT_DEREFERENCE) .arg(Arg::with_name(OPT_DEREFERENCE)
.short("L") .short("L")
.long(OPT_DEREFERENCE) .long(OPT_DEREFERENCE)
@ -548,12 +552,22 @@ impl FromStr for Attribute {
} }
} }
fn add_all_attributes() -> Vec<Attribute> {
let mut attr = Vec::new();
#[cfg(unix)]
attr.push(Attribute::Mode);
attr.push(Attribute::Ownership);
attr.push(Attribute::Timestamps);
attr.push(Attribute::Context);
attr.push(Attribute::Xattr);
attr.push(Attribute::Links);
attr
}
impl Options { impl Options {
fn from_matches(matches: &ArgMatches) -> CopyResult<Options> { fn from_matches(matches: &ArgMatches) -> CopyResult<Options> {
let not_implemented_opts = vec![ let not_implemented_opts = vec![
OPT_ARCHIVE,
OPT_COPY_CONTENTS, OPT_COPY_CONTENTS,
OPT_NO_DEREFERENCE_PRESERVE_LINKS,
OPT_DEREFERENCE, OPT_DEREFERENCE,
OPT_PARENTS, OPT_PARENTS,
OPT_SPARSE, OPT_SPARSE,
@ -590,13 +604,7 @@ impl Options {
let mut attributes = Vec::new(); let mut attributes = Vec::new();
for attribute_str in attribute_strs { for attribute_str in attribute_strs {
if attribute_str == "all" { if attribute_str == "all" {
#[cfg(unix)] attributes = add_all_attributes();
attributes.push(Attribute::Mode);
attributes.push(Attribute::Ownership);
attributes.push(Attribute::Timestamps);
attributes.push(Attribute::Context);
attributes.push(Attribute::Xattr);
attributes.push(Attribute::Links);
break; break;
} else { } else {
attributes.push(Attribute::from_str(attribute_str)?); attributes.push(Attribute::from_str(attribute_str)?);
@ -605,6 +613,11 @@ impl Options {
attributes attributes
} }
} }
} else if matches.is_present(OPT_ARCHIVE) {
// --archive is used. Same as --preserve=all
add_all_attributes()
} else if matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) {
vec![Attribute::Links]
} else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) { } else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) {
DEFAULT_ATTRIBUTES.to_vec() DEFAULT_ATTRIBUTES.to_vec()
} else { } else {
@ -616,7 +629,10 @@ impl Options {
copy_contents: matches.is_present(OPT_COPY_CONTENTS), copy_contents: matches.is_present(OPT_COPY_CONTENTS),
copy_mode: CopyMode::from_matches(matches), copy_mode: CopyMode::from_matches(matches),
dereference: matches.is_present(OPT_DEREFERENCE), dereference: matches.is_present(OPT_DEREFERENCE),
no_dereference: matches.is_present(OPT_NO_DEREFERENCE), // No dereference is set with -p, -d and --archive
no_dereference: matches.is_present(OPT_NO_DEREFERENCE)
|| matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
|| matches.is_present(OPT_ARCHIVE),
one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM),
overwrite: OverwriteMode::from_matches(matches), overwrite: OverwriteMode::from_matches(matches),
parents: matches.is_present(OPT_PARENTS), parents: matches.is_present(OPT_PARENTS),
@ -797,7 +813,6 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
let dest = construct_dest_path(source, target, &target_type, options)?; let dest = construct_dest_path(source, target, &target_type, options)?;
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap(); preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
} }
if !found_hard_link { if !found_hard_link {
if let Err(error) = copy_source(source, target, &target_type, options) { if let Err(error) = copy_source(source, target, &target_type, options) {
show_error!("{}", error); show_error!("{}", error);
@ -953,7 +968,19 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult
let dest = local_to_target.as_path().to_path_buf(); let dest = local_to_target.as_path().to_path_buf();
preserve_hardlinks(&mut hard_links, &source, dest, &mut found_hard_link).unwrap(); preserve_hardlinks(&mut hard_links, &source, dest, &mut found_hard_link).unwrap();
if !found_hard_link { if !found_hard_link {
copy_file(path.as_path(), local_to_target.as_path(), options)?; match copy_file(path.as_path(), local_to_target.as_path(), options) {
Ok(_) => Ok(()),
Err(err) => {
if fs::symlink_metadata(&source)?.file_type().is_symlink() {
// silent the error with a symlink
// In case we do --archive, we might copy the symlink
// before the file itself
Ok(())
} else {
Err(err)
}
}
}?;
} }
} else { } else {
copy_file(path.as_path(), local_to_target.as_path(), options)?; copy_file(path.as_path(), local_to_target.as_path(), options)?;
@ -1026,12 +1053,16 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu
} }
} }
}; };
Ok(()) Ok(())
} }
#[cfg(not(windows))] #[cfg(not(windows))]
fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> {
Ok(std::os::unix::fs::symlink(source, dest).context(context)?) match std::os::unix::fs::symlink(source, dest).context(context) {
Ok(_) => Ok(()),
Err(_) => Ok(()),
}
} }
#[cfg(windows)] #[cfg(windows)]
@ -1144,11 +1175,9 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
.unwrap(); .unwrap();
} }
}; };
for attribute in &options.preserve_attributes { for attribute in &options.preserve_attributes {
copy_attribute(source, dest, attribute)?; copy_attribute(source, dest, attribute)?;
} }
Ok(()) Ok(())
} }

View file

@ -8,8 +8,16 @@ use std::os::unix::fs;
#[cfg(windows)] #[cfg(windows)]
use std::os::windows::fs::symlink_file; use std::os::windows::fs::symlink_file;
#[cfg(target_os = "linux")]
use filetime::FileTime;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::env; use std::env;
#[cfg(target_os = "linux")]
use std::fs as std_fs;
#[cfg(target_os = "linux")]
use std::thread::sleep;
#[cfg(target_os = "linux")]
use std::time::Duration;
static TEST_EXISTING_FILE: &str = "existing_file.txt"; static TEST_EXISTING_FILE: &str = "existing_file.txt";
static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt"; static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt";
@ -209,7 +217,7 @@ fn test_cp_arg_interactive() {
} }
#[test] #[test]
#[cfg(target_os = "unix")] #[cfg(target_os = "linux")]
fn test_cp_arg_link() { fn test_cp_arg_link() {
use std::os::linux::fs::MetadataExt; use std::os::linux::fs::MetadataExt;
@ -491,3 +499,220 @@ fn test_cp_no_deref_folder_to_folder() {
let path_to_check = path_to_new_symlink.to_str().unwrap(); let path_to_check = path_to_new_symlink.to_str().unwrap();
assert_eq!(at.read(&path_to_check), "Hello, World!\n"); assert_eq!(at.read(&path_to_check), "Hello, World!\n");
} }
#[test]
#[cfg(target_os = "linux")]
fn test_cp_archive() {
let (at, mut ucmd) = at_and_ucmd!();
let ts = time::now().to_timespec();
let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32);
// set the file creation/modif an hour ago
filetime::set_file_times(
at.plus_as_string(TEST_HELLO_WORLD_SOURCE),
previous,
previous,
)
.unwrap();
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--archive")
.arg(TEST_HOW_ARE_YOU_SOURCE)
.run();
assert!(result.success);
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap();
let creation = metadata.modified().unwrap();
let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap();
let creation2 = metadata2.modified().unwrap();
let scene2 = TestScenario::new("ls");
let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run();
println!("ls dest {}", result.stdout);
assert_eq!(creation, creation2);
assert!(result.success);
}
#[test]
#[cfg(target_os = "unix")]
fn test_cp_archive_recursive() {
let (at, mut ucmd) = at_and_ucmd!();
let cwd = env::current_dir().unwrap();
// creates
// dir/1
// dir/1.link => dir/1
// dir/2
// dir/2.link => dir/2
let file_1 = at.subdir.join(TEST_COPY_TO_FOLDER).join("1");
let file_1_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("1.link");
let file_2 = at.subdir.join(TEST_COPY_TO_FOLDER).join("2");
let file_2_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("2.link");
at.touch(&file_1.to_string_lossy());
at.touch(&file_2.to_string_lossy());
// Change the cwd to have a correct symlink
assert!(env::set_current_dir(&at.subdir.join(TEST_COPY_TO_FOLDER)).is_ok());
#[cfg(not(windows))]
{
let _r = fs::symlink("1", &file_1_link);
let _r = fs::symlink("2", &file_2_link);
}
#[cfg(windows)]
{
let _r = symlink_file("1", &file_1_link);
let _r = symlink_file("2", &file_2_link);
}
// Back to the initial cwd (breaks the other tests)
assert!(env::set_current_dir(&cwd).is_ok());
let resultg = ucmd
.arg("--archive")
.arg(TEST_COPY_TO_FOLDER)
.arg(TEST_COPY_TO_FOLDER_NEW)
.run();
let scene2 = TestScenario::new("ls");
let result = scene2
.cmd("ls")
.arg("-al")
.arg(&at.subdir.join(TEST_COPY_TO_FOLDER))
.run();
println!("ls dest {}", result.stdout);
let scene2 = TestScenario::new("ls");
let result = scene2
.cmd("ls")
.arg("-al")
.arg(&at.subdir.join(TEST_COPY_TO_FOLDER_NEW))
.run();
println!("ls dest {}", result.stdout);
assert!(at.file_exists(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("1.link")
.to_string_lossy()
));
assert!(at.file_exists(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("2.link")
.to_string_lossy()
));
assert!(at.file_exists(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("1")
.to_string_lossy()
));
assert!(at.file_exists(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("2")
.to_string_lossy()
));
assert!(at.is_symlink(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("1.link")
.to_string_lossy()
));
assert!(at.is_symlink(
&at.subdir
.join(TEST_COPY_TO_FOLDER_NEW)
.join("2.link")
.to_string_lossy()
));
// fails for now
assert!(resultg.success);
}
#[test]
#[cfg(target_os = "linux")]
fn test_cp_preserve_timestamps() {
let (at, mut ucmd) = at_and_ucmd!();
let ts = time::now().to_timespec();
let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32);
// set the file creation/modif an hour ago
filetime::set_file_times(
at.plus_as_string(TEST_HELLO_WORLD_SOURCE),
previous,
previous,
)
.unwrap();
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--preserve=timestamps")
.arg(TEST_HOW_ARE_YOU_SOURCE)
.run();
assert!(result.success);
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap();
let creation = metadata.modified().unwrap();
let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap();
let creation2 = metadata2.modified().unwrap();
let scene2 = TestScenario::new("ls");
let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run();
println!("ls dest {}", result.stdout);
assert_eq!(creation, creation2);
assert!(result.success);
}
#[test]
#[cfg(target_os = "linux")]
fn test_cp_dont_preserve_timestamps() {
let (at, mut ucmd) = at_and_ucmd!();
let ts = time::now().to_timespec();
let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32);
// set the file creation/modif an hour ago
filetime::set_file_times(
at.plus_as_string(TEST_HELLO_WORLD_SOURCE),
previous,
previous,
)
.unwrap();
sleep(Duration::from_secs(3));
let result = ucmd
.arg(TEST_HELLO_WORLD_SOURCE)
.arg("--no-preserve=timestamps")
.arg(TEST_HOW_ARE_YOU_SOURCE)
.run();
assert!(result.success);
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap();
let creation = metadata.modified().unwrap();
let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap();
let creation2 = metadata2.modified().unwrap();
let scene2 = TestScenario::new("ls");
let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run();
println!("ls dest {}", result.stdout);
println!("creation {:?} / {:?}", creation, creation2);
assert_ne!(creation, creation2);
let res = creation.elapsed().unwrap() - creation2.elapsed().unwrap();
// Some margins with time check
assert!(res.as_secs() > 3595);
assert!(res.as_secs() < 3605);
assert!(result.success);
}