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

Merge pull request #1066 from Matt8898/master

cp: add support for --attributes-only and setting timestamps, links and xattrs
This commit is contained in:
Alex Lyon 2017-10-01 01:43:09 -07:00 committed by GitHub
commit ab259194f3
2 changed files with 204 additions and 55 deletions

View file

@ -17,10 +17,18 @@ walkdir = "1.0.7"
clap = "2.20.0"
quick-error = "1.1.0"
uucore = { path="../uucore" }
filetime = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
ioctl-sys = "0.5.2"
[target.'cfg(target_os = "windows")'.dependencies]
kernel32-sys = "*"
winapi = "*"
[target.'cfg(unix)'.dependencies]
xattr="0.2.1"
[[bin]]
name = "cp"
path = "main.rs"

View file

@ -10,13 +10,31 @@
* that was distributed with this source code.
*/
extern crate libc;
extern crate clap;
extern crate walkdir;
extern crate filetime;
#[cfg(target_os = "linux")]
#[macro_use] extern crate ioctl_sys;
#[macro_use] extern crate uucore;
#[macro_use] extern crate quick_error;
#[cfg(unix)]
extern crate xattr;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
#[cfg(windows)]
extern crate kernel32;
#[cfg(windows)]
use kernel32::GetFileInformationByHandle;
#[cfg(windows)]
use kernel32::CreateFile2;
#[cfg(windows)]
extern crate winapi;
use std::mem;
use std::ffi::CString;
use clap::{Arg, App, ArgMatches};
use quick_error::ResultExt;
use std::collections::HashSet;
@ -30,6 +48,10 @@ use walkdir::WalkDir;
#[cfg(target_os = "linux")] use std::os::unix::io::IntoRawFd;
use std::fs::File;
use std::fs::OpenOptions;
use filetime::FileTime;
#[cfg(target_os = "linux")]
use libc::{c_int, c_char};
#[cfg(unix)] use std::os::unix::fs::PermissionsExt;
@ -157,9 +179,10 @@ pub enum CopyMode {
Sparse,
Copy,
Update,
AttrOnly
}
#[derive(Clone)]
#[derive(Clone, Eq, PartialEq)]
pub enum Attribute {
#[cfg(unix)] Mode,
Ownership,
@ -167,7 +190,6 @@ pub enum Attribute {
Context,
Links,
Xattr,
All,
}
/// Re-usable, extensible copy options
@ -231,7 +253,7 @@ static OPT_ONE_FILE_SYSTEM: &str = "one-file-system";
static OPT_PARENTS: &str = "parents";
static OPT_PATHS: &str = "paths";
static OPT_PRESERVE: &str = "preserve";
static OPT_PRESERVE_DEFUALT_ATTRIBUTES: &str = "preserve-default-attributes";
static OPT_PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes";
static OPT_RECURSIVE: &str = "recursive";
static OPT_RECURSIVE_ALIAS: &str = "recursive_alias";
static OPT_REFLINK: &str = "reflink";
@ -343,18 +365,39 @@ pub fn uumain(args: Vec<String>) -> i32 {
.takes_value(true)
.value_name("WHEN")
.help("control clone/CoW copies. See below"))
.arg(Arg::with_name(OPT_ATTRIBUTES_ONLY)
.long(OPT_ATTRIBUTES_ONLY)
.conflicts_with(OPT_COPY_CONTENTS)
.overrides_with(OPT_REFLINK)
.help("Don't copy the file data, just the attributes"))
.arg(Arg::with_name(OPT_PRESERVE)
.long(OPT_PRESERVE)
.takes_value(true)
.multiple(true)
.use_delimiter(true)
.possible_values(PRESERVABLE_ATTRIBUTES)
.value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE])
.help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\
if possible additional attributes: context, links, xattr, all"))
.arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES)
.short("-p")
.long(OPT_PRESERVE_DEFAULT_ATTRIBUTES)
.conflicts_with_all(&[OPT_PRESERVE, OPT_NO_PRESERVE, OPT_ARCHIVE])
.help("same as --preserve=mode(unix only),ownership,timestamps"))
.arg(Arg::with_name(OPT_NO_PRESERVE)
.long(OPT_NO_PRESERVE)
.takes_value(true)
.value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE])
.help("don't preserve the specified attributes"))
// TODO: implement the following args
.arg(Arg::with_name(OPT_ARCHIVE)
.short("a")
.long(OPT_ARCHIVE)
.conflicts_with_all(&[OPT_PRESERVE_DEFUALT_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"))
.arg(Arg::with_name(OPT_ATTRIBUTES_ONLY)
.long(OPT_ATTRIBUTES_ONLY)
.conflicts_with(OPT_COPY_CONTENTS)
.overrides_with(OPT_REFLINK)
.help("NotImplemented: don't copy the file data, just the attributes"))
.arg(Arg::with_name(OPT_COPY_CONTENTS)
.long(OPT_COPY_CONTENTS)
.conflicts_with(OPT_ATTRIBUTES_ONLY)
@ -372,26 +415,6 @@ pub fn uumain(args: Vec<String>) -> i32 {
.long(OPT_NO_DEREFERENCE)
.conflicts_with(OPT_DEREFERENCE)
.help("NotImplemented: never follow symbolic links in SOURCE"))
.arg(Arg::with_name(OPT_PRESERVE_DEFUALT_ATTRIBUTES)
.short("-p")
.long(OPT_PRESERVE_DEFUALT_ATTRIBUTES)
.conflicts_with_all(&[OPT_PRESERVE, OPT_NO_PRESERVE, OPT_ARCHIVE])
.help("NotImplemented: same as --preserve=mode(unix only),ownership,timestamps"))
.arg(Arg::with_name(OPT_PRESERVE)
.long(OPT_PRESERVE)
.takes_value(true)
.multiple(true)
.possible_values(PRESERVABLE_ATTRIBUTES)
.value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFUALT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE])
.help("NotImplemented: preserve the specified attributes (default: mode(unix only),ownership,timestamps),\
if possible additional attributes: context, links, xattr, all"))
.arg(Arg::with_name(OPT_NO_PRESERVE)
.long(OPT_NO_PRESERVE)
.takes_value(true)
.value_name("ATTR_LIST")
.conflicts_with_all(&[OPT_PRESERVE_DEFUALT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE])
.help("NotImplemented: don't preserve the specified attributes"))
.arg(Arg::with_name(OPT_PARENTS)
.long(OPT_PARENTS)
.help("NotImplemented: use full source file name under DIRECTORY"))
@ -481,6 +504,8 @@ impl CopyMode {
CopyMode::Sparse
} else if matches.is_present(OPT_UPDATE) {
CopyMode::Update
} else if matches.is_present(OPT_ATTRIBUTES_ONLY) {
CopyMode::AttrOnly
} else {
CopyMode::Copy
}
@ -498,7 +523,6 @@ impl FromStr for Attribute {
"context" => Attribute::Context,
"links" => Attribute::Links,
"xattr" => Attribute::Xattr,
"all" => Attribute::All,
_ => return Err(Error::InvalidArgument(format!("invalid attribute '{}'", value)))
})
}
@ -508,14 +532,10 @@ impl Options {
fn from_matches(matches: &ArgMatches) -> CopyResult<Options> {
let not_implemented_opts = vec![
OPT_ARCHIVE,
OPT_ATTRIBUTES_ONLY,
OPT_COPY_CONTENTS,
OPT_NO_DEREFERENCE_PRESERVE_LINKS,
OPT_DEREFERENCE,
OPT_NO_DEREFERENCE,
OPT_PRESERVE_DEFUALT_ATTRIBUTES,
OPT_PRESERVE,
OPT_NO_PRESERVE,
OPT_PARENTS,
OPT_SPARSE,
OPT_STRIP_TRAILING_SLASHES,
@ -548,16 +568,28 @@ impl Options {
Some(attribute_strs) => {
let mut attributes = Vec::new();
for attribute_str in attribute_strs {
if attribute_str == "all" {
#[cfg(unix)]
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;
} else {
attributes.push(Attribute::from_str(attribute_str)?);
}
}
attributes
}
}
} else if matches.is_present(OPT_PRESERVE_DEFUALT_ATTRIBUTES) {
} else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) {
DEFAULT_ATTRIBUTES.to_vec()
} else {
vec![]
};
let options = Options {
attributes_only: matches.is_present(OPT_ATTRIBUTES_ONLY),
copy_contents: matches.is_present(OPT_COPY_CONTENTS),
@ -598,7 +630,6 @@ impl Options {
}
}
impl TargetType {
/// Return TargetType required for `target`.
///
@ -646,6 +677,49 @@ fn parse_path_args(path_args: &[String], options: &Options) -> CopyResult<(Vec<S
Ok((sources, target))
}
fn preserve_hardlinks(hard_links: &mut Vec<(String, u64)>, source: &std::path::PathBuf, dest: std::path::PathBuf, found_hard_link: &mut bool) -> CopyResult<()> {
if !source.is_dir() {
unsafe {
let src_path = CString::new(source.as_os_str().to_str().unwrap()).unwrap();
let mut inode: u64 = 0;
let mut nlinks = 0;
#[cfg(unix)]
{
let mut stat = mem::zeroed();
if libc::lstat(src_path.as_ptr(), &mut stat) < 0 {
return Err(format!("cannot stat {:?}: {}", src_path, std::io::Error::last_os_error()).into());
}
inode = stat.st_ino;
nlinks = stat.st_nlink;
}
#[cfg(windows)]
{
let mut stat = mem::uninitialized();
let handle = CreateFile2(src_path.as_ptr() as *const u16,
winapi::winnt::GENERIC_READ,
winapi::winnt::FILE_SHARE_READ,
0,
std::ptr::null_mut());
if GetFileInformationByHandle(handle, stat) != 0 {
return Err(format!("cannot get file information {:?}: {}", source, std::io::Error::last_os_error()).into());
}
inode = (((*stat).nFileIndexHigh as u64) << 32 | (*stat).nFileIndexLow as u64);
nlinks = (*stat).nNumberOfLinks;
}
for hard_link in hard_links.iter() {
if hard_link.1 == inode {
std::fs::hard_link(hard_link.0.clone(), dest.clone()).unwrap();
*found_hard_link = true;
}
}
if !(*found_hard_link) && nlinks > 1 {
hard_links.push((dest.clone().to_str().unwrap().to_string(), inode));
}
}
}
Ok(())
}
/// Copy all `sources` to `target`. Returns an
/// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was
@ -658,23 +732,39 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
let target_type = TargetType::determine(sources, target);
verify_target_type(target, &target_type)?;
let mut preserve_hard_links = false;
for attribute in &options.preserve_attributes {
if *attribute == Attribute::Links {
preserve_hard_links = true;
}
}
let mut hard_links: Vec<(String, u64)> = vec![];
let mut non_fatal_errors = false;
let mut seen_sources = HashSet::with_capacity(sources.len());
for source in sources {
if seen_sources.contains(source) {
show_warning!("source '{}' specified more than once", source.display());
} else if let Err(error) = copy_source(source, target, &target_type, options) {
} else {
let mut found_hard_link = false;
if preserve_hard_links {
let dest = construct_dest_path(source, target, &target_type, options)?;
let src_path = CString::new(Path::new(&source.clone()).as_os_str().to_str().unwrap()).unwrap();
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
}
if !found_hard_link {
if let Err(error) = copy_source(source, target, &target_type, options) {
show_error!("{}", error);
match error {
Error::Skipped(_) => (),
_ => non_fatal_errors = true,
}
}
}
seen_sources.insert(source);
}
}
if non_fatal_errors {
Err(Error::NotAllFilesCopied)
} else {
@ -703,7 +793,6 @@ fn copy_source(source: &Source, target: &Target, target_type: &TargetType, optio
-> CopyResult<()>
{
let source_path = Path::new(&source);
if source_path.is_dir() {
// Copy as directory
copy_directory(source, target, options)
@ -733,6 +822,18 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult
Some(root_path.as_path())
};
#[cfg(unix)]
let mut hard_links: Vec<(String, u64)> = vec![];
let mut preserve_hard_links = false;
for attribute in &options.preserve_attributes {
if *attribute == Attribute::Links {
preserve_hard_links = true;
}
}
#[cfg(windows)]
let mut hard_links: Vec<(String, u64)> = vec![];
for path in WalkDir::new(root) {
let path = or_continue!(or_continue!(path).path().canonicalize());
let local_to_root_parent = match root_parent {
@ -745,14 +846,23 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult
if path.is_dir() && !local_to_target.exists() {
or_continue!(fs::create_dir_all(local_to_target.clone()));
} else if !path.is_dir() {
if preserve_hard_links {
let mut found_hard_link = false;
let source = 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();
if !found_hard_link {
copy_file(path.as_path(), local_to_target.as_path(), options)?;
}
} else {
copy_file(path.as_path(), local_to_target.as_path(), options)?;
}
}
}
Ok(())
}
impl OverwriteMode {
fn verify(&self, path: &Path) -> CopyResult<()> {
match *self {
@ -785,11 +895,27 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu
let metadata = fs::metadata(source).context(context)?;
fs::set_permissions(dest, metadata.permissions()).context(context)?;
},
Attribute::Timestamps => return Err(Error::NotImplemented("preserving timestamp not implemented".to_string())),
Attribute::Context => return Err(Error::NotImplemented("preserving context not implemented".to_string())),
Attribute::Links => return Err(Error::NotImplemented("preserving links not implemented".to_string())),
Attribute::Xattr => return Err(Error::NotImplemented("preserving xattr not implemented".to_string())),
Attribute::All => return Err(Error::NotImplemented("preserving a not implemented".to_string())),
Attribute::Timestamps => {
let metadata = fs::metadata(source)?;
filetime::set_file_times(Path::new(dest), FileTime::from_last_access_time(&metadata), FileTime::from_last_modification_time(&metadata))?;
},
Attribute::Context => {},
Attribute::Links => {},
Attribute::Xattr => {
#[cfg(unix)]
{
let xattrs = xattr::list(source)?;
for attr in xattrs {
if let Some(attr_value) = xattr::get(source, attr.clone())? {
xattr::set(dest, attr, &attr_value[..]);
}
}
}
#[cfg(not(unix))]
{
return Err(format!("XAttrs are only supported on unix.").into());
}
},
})
}
@ -862,6 +988,13 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
println!("{}", context_for(source, dest));
}
let mut preserve_context = false;
for attribute in &options.preserve_attributes {
if *attribute == Attribute::Context {
preserve_context = true;
}
}
match options.copy_mode {
CopyMode::Link => {
fs::hard_link(source, dest).context(&*context_for(source, dest))?;
@ -888,6 +1021,14 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
} else {
copy_helper(source, dest, options)?;
}
},
CopyMode::AttrOnly => {
OpenOptions::new()
.write(true)
.truncate(false)
.create(true)
.open(dest)
.unwrap();
}
};