Subject: [PATCH] cp: add an option to only copy the file attributes

From: Pádraig Brady <[email-masked]>
Date: Wed, 14 Apr 2010 16:34:35 +0100

>From the NEWS of the attached patch:

"cp now accepts the --attributes-only option to not copy file data,
which is useful for efficiently modifying files."

For an example script that uses this option see:
http://lists.gnu.org/archive/html/bug-coreutils/2010-03/msg00261.html

cheers,
Pádraig.
>From 09e1d49eb1e853487fc983176660908b133fb2d9 Mon Sep 17 00:00:00 2001
From: =?utf-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Mon, 21 Sep 2009 08:43:03 +0100
Subject: [PATCH] cp: add an option to only copy the file attributes

* src/copy.c (copy_attr): A new function which merges copy_attr_by_fd
and copy_attr_by_name.  Display all errors when only copying attributes
* src/copy.c (copy_reg): Skip copying the file contents if specified.
Refactor the SELinux error handling code a little and display all
SELinux errors when only copying attributes.
* src/copy.h (struct cp_options): Add a data_copy_required boolean
* src/cp.c (main): Default to copying data but don't if specified
* src/install.c: Default to copying data
* src/mv.c: Likewise
tests/cp/reflink-perm: Add a test to check that --attributes-only
does not copy data
* tests/cp/acl: Likewise. Also refactor to remove redundant
acl manipulation
* doc/coreutils.texi (cp invocation): Describe the new option
* NEWS: Mention the new feature
---
 NEWS                  |    3 ++
 doc/coreutils.texi    |    6 +++
 src/copy.c            |   88 +++++++++++++++++++++++--------------------------
 src/copy.h            |    4 ++
 src/cp.c              |   10 +++++-
 src/install.c         |    1 +
 src/mv.c              |    1 +
 tests/cp/acl          |   19 ++++++----
 tests/cp/reflink-perm |    4 ++
 9 files changed, 80 insertions(+), 56 deletions(-)

diff --git a/NEWS b/NEWS
index 2be9633..a50c35d 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,9 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** New features
 
+  cp now accepts the --attributes-only option to not copy file data,
+  which is useful for efficiently modifying files.
+
   join now accepts the --header option, to treat the first line of each
   file as a header line to be joined and printed unconditionally.
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 7561f2a..a3748e4 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -7346,6 +7346,12 @@ Try to preserve SELinux security context and extended attributes (xattr),
 but ignore any failure to do that and print no corresponding diagnostic.
 Equivalent to @option{-dR --preserve=all} with the reduced diagnostics.
 
+@itemx --attributes-only
+@opindex --attributes-only
+Preserve the specified attributes of the original files in the copy,
+but do not copy any data.  See the @option{--preserve} option for
+controlling which attributes to copy.
+
 @item -b
 @itemx @w{@kbd{--backup}[=@var{method}]}
 @opindex -b
diff --git a/src/copy.c b/src/copy.c
index 0fa148e..381bdc6 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -179,11 +179,11 @@ static void
 copy_attr_error (struct error_context *ctx ATTRIBUTE_UNUSED,
                  char const *fmt, ...)
 {
-  int err = errno;
-  va_list ap;
-
   if (!errno_unsupported (errno))
     {
+      int err = errno;
+      va_list ap;
+
       /* use verror module to print error message */
       va_start (ap, fmt);
       verror (0, err, fmt, ap);
@@ -216,51 +216,43 @@ copy_attr_free (struct error_context *ctx ATTRIBUTE_UNUSED,
 {
 }
 
-static bool
-copy_attr_by_fd (char const *src_path, int src_fd,
-                 char const *dst_path, int dst_fd, const struct cp_options *x)
-{
-  struct error_context ctx =
-  {
-    .error = x->require_preserve_xattr ? copy_attr_allerror : copy_attr_error,
-    .quote = copy_attr_quote,
-    .quote_free = copy_attr_free
-  };
-  return 0 == attr_copy_fd (src_path, src_fd, dst_path, dst_fd, 0,
-                            (x->reduce_diagnostics
-                             && !x->require_preserve_xattr)? NULL : &ctx);
-}
+/* If valid SRC_FD and DST_FD descriptors are passed,
+   then copy by fd, otherwise copy by name.  */
 
 static bool
-copy_attr_by_name (char const *src_path, char const *dst_path,
-                   const struct cp_options *x)
+copy_attr (char const *src_path, int src_fd,
+           char const *dst_path, int dst_fd, const struct cp_options *x)
 {
+  int ret;
+  bool all_errors = (!x->data_copy_required || x->require_preserve_xattr);
+  bool some_errors = (!all_errors && !x->reduce_diagnostics);
   struct error_context ctx =
   {
-    .error = x->require_preserve_xattr ? copy_attr_allerror : copy_attr_error,
+    .error = all_errors ? copy_attr_allerror : copy_attr_error,
     .quote = copy_attr_quote,
     .quote_free = copy_attr_free
   };
-  return 0 == attr_copy_file (src_path, dst_path, 0,
-                              (x-> reduce_diagnostics
-                               && !x->require_preserve_xattr) ? NULL : &ctx);
-}
-#else /* USE_XATTR */
+  if (src_fd > 0 && dst_fd > 0)
+    {
+      ret = attr_copy_fd (src_path, src_fd, dst_path, dst_fd, 0,
+                          ((all_errors || some_errors) ? &ctx : NULL));
+    }
+  else
+    {
+      ret = attr_copy_file (src_path, dst_path, 0,
+                            ((all_errors || some_errors) ? &ctx : NULL));
+    }
 
-static bool
-copy_attr_by_fd (char const *src_path ATTRIBUTE_UNUSED,
-                 int src_fd ATTRIBUTE_UNUSED,
-                 char const *dst_path ATTRIBUTE_UNUSED,
-                 int dst_fd ATTRIBUTE_UNUSED,
-                 const struct cp_options *x ATTRIBUTE_UNUSED)
-{
-  return true;
+  return ret == 0;
 }
+#else /* USE_XATTR */
 
 static bool
-copy_attr_by_name (char const *src_path ATTRIBUTE_UNUSED,
-                   char const *dst_path ATTRIBUTE_UNUSED,
-                   const struct cp_options *x ATTRIBUTE_UNUSED)
+copy_attr (char const *src_path ATTRIBUTE_UNUSED,
+           int src_fd ATTRIBUTE_UNUSED,
+           char const *dst_path ATTRIBUTE_UNUSED,
+           int dst_fd ATTRIBUTE_UNUSED,
+           const struct cp_options *x ATTRIBUTE_UNUSED)
 {
   return true;
 }
@@ -485,7 +477,7 @@ copy_reg (char const *src_name, char const *dst_name,
   struct stat sb;
   struct stat src_open_sb;
   bool return_val = true;
-  bool data_copy_required = true;
+  bool data_copy_required = x->data_copy_required;
 
   source_desc = open (src_name,
                       (O_RDONLY | O_BINARY
@@ -528,11 +520,14 @@ copy_reg (char const *src_name, char const *dst_name,
          that is used when the destination file doesn't already exist.  */
       if (x->preserve_security_context && 0 <= dest_desc)
         {
+          bool all_errors = !x->data_copy_required ||
+                            x->require_preserve_context;
+          bool some_errors = !all_errors && !x->reduce_diagnostics;
           security_context_t con = NULL;
+
           if (getfscreatecon (&con) < 0)
             {
-              if (x->require_preserve_context ||
-                  (!x->reduce_diagnostics && !errno_unsupported (errno)))
+              if (all_errors || (some_errors && !errno_unsupported (errno)))
                 error (0, errno, _("failed to get file system create context"));
               if (x->require_preserve_context)
                 {
@@ -545,8 +540,7 @@ copy_reg (char const *src_name, char const *dst_name,
             {
               if (fsetfilecon (dest_desc, con) < 0)
                 {
-                  if (x->require_preserve_context ||
-                      (!x->reduce_diagnostics && !errno_unsupported (errno)))
+                  if (all_errors || (some_errors && !errno_unsupported (errno)))
                     error (0, errno,
                            _("failed to set the security context of %s to %s"),
                            quote_n (0, dst_name), quote_n (1, con));
@@ -836,7 +830,7 @@ copy_reg (char const *src_name, char const *dst_name,
       if (!(sb.st_mode & S_IWUSR) && geteuid () != 0)
         access_changed = fchmod_or_lchmod (dest_desc, dst_name, 0600) == 0;
 
-      if (!copy_attr_by_fd (src_name, source_desc, dst_name, dest_desc, x)
+      if (!copy_attr (src_name, source_desc, dst_name, dest_desc, x)
           && x->require_preserve_xattr)
         return_val = false;
 
@@ -1821,14 +1815,15 @@ copy_internal (char const *src_name, char const *dst_name,
 
   if (x->preserve_security_context)
     {
+      bool all_errors = !x->data_copy_required || x->require_preserve_context;
+      bool some_errors = !all_errors && !x->reduce_diagnostics;
       security_context_t con;
 
       if (0 <= lgetfilecon (src_name, &con))
         {
           if (setfscreatecon (con) < 0)
             {
-              if (x->require_preserve_context ||
-                  (!x->reduce_diagnostics && !errno_unsupported (errno)))
+              if (all_errors || (some_errors && !errno_unsupported (errno)))
                 error (0, errno,
                        _("failed to set default file creation context to %s"),
                        quote (con));
@@ -1842,8 +1837,7 @@ copy_internal (char const *src_name, char const *dst_name,
         }
       else
         {
-          if (x->require_preserve_context ||
-              (!x->reduce_diagnostics && !errno_unsupported (errno)))
+          if (all_errors || (some_errors && !errno_unsupported (errno)))
             {
               error (0, errno,
                      _("failed to get security context of %s"),
@@ -2168,7 +2162,7 @@ copy_internal (char const *src_name, char const *dst_name,
 
   set_author (dst_name, -1, &src_sb);
 
-  if (x->preserve_xattr && ! copy_attr_by_name (src_name, dst_name, x)
+  if (x->preserve_xattr && ! copy_attr (src_name, -1, dst_name, -1, x)
       && x->require_preserve_xattr)
     return false;
 
diff --git a/src/copy.h b/src/copy.h
index 59e29f5..5782431 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -170,6 +170,10 @@ struct cp_options
      will be hard links to the same file (a copy of F).  */
   bool preserve_links;
 
+  /* Optionally copy the data either with CoW reflink files or
+     explicitly with the --attributes-only option.  */
+  bool data_copy_required;
+
   /* If true and any of the above (for preserve) file attributes cannot
      be applied to a destination file, treat it as a failure and return
      nonzero immediately.  E.g. for cp -p this must be true, for mv it
diff --git a/src/cp.c b/src/cp.c
index cc958d1..13ce9e0 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -72,7 +72,8 @@ struct dir_attr
    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
 enum
 {
-  COPY_CONTENTS_OPTION = CHAR_MAX + 1,
+  ATTRIBUTES_ONLY_OPTION = CHAR_MAX + 1,
+  COPY_CONTENTS_OPTION,
   NO_PRESERVE_ATTRIBUTES_OPTION,
   PARENTS_OPTION,
   PRESERVE_ATTRIBUTES_OPTION,
@@ -115,6 +116,7 @@ ARGMATCH_VERIFY (reflink_type_string, reflink_type);
 static struct option const long_opts[] =
 {
   {"archive", no_argument, NULL, 'a'},
+  {"attributes-only", no_argument, NULL, ATTRIBUTES_ONLY_OPTION},
   {"backup", optional_argument, NULL, 'b'},
   {"copy-contents", no_argument, NULL, COPY_CONTENTS_OPTION},
   {"dereference", no_argument, NULL, 'L'},
@@ -167,6 +169,7 @@ Mandatory arguments to long options are mandatory for short options too.\n\
 "), stdout);
       fputs (_("\
   -a, --archive                same as -dR --preserve=all\n\
+      --attributes-only        don't copy the file data, just the attributes\n\
       --backup[=CONTROL]       make a backup of each existing destination file\n\
   -b                           like --backup but does not accept an argument\n\
       --copy-contents          copy contents of special files when recursive\n\
@@ -781,6 +784,7 @@ cp_option_init (struct cp_options *x)
   x->reduce_diagnostics = false;
   x->require_preserve_xattr = false;
 
+  x->data_copy_required = true;
   x->require_preserve = false;
   x->recursive = false;
   x->sparse_mode = SPARSE_AUTO;
@@ -962,6 +966,10 @@ main (int argc, char **argv)
             version_control_string = optarg;
           break;
 
+        case ATTRIBUTES_ONLY_OPTION:
+          x.data_copy_required = false;
+          break;
+
         case COPY_CONTENTS_OPTION:
           copy_contents = true;
           break;
diff --git a/src/install.c b/src/install.c
index bac5c7c..bef7f8a 100644
--- a/src/install.c
+++ b/src/install.c
@@ -282,6 +282,7 @@ cp_option_init (struct cp_options *x)
   x->preserve_mode = false;
   x->preserve_timestamps = false;
   x->reduce_diagnostics=false;
+  x->data_copy_required = true;
   x->require_preserve = false;
   x->require_preserve_context = false;
   x->require_preserve_xattr = false;
diff --git a/src/mv.c b/src/mv.c
index 8a41d16..91aadfa 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -119,6 +119,7 @@ cp_option_init (struct cp_options *x)
   x->preserve_timestamps = true;
   x->preserve_security_context = selinux_enabled;
   x->reduce_diagnostics = false;
+  x->data_copy_required = true;
   x->require_preserve = false;  /* FIXME: maybe make this an option */
   x->require_preserve_context = false;
   x->preserve_xattr = true;
diff --git a/tests/cp/acl b/tests/cp/acl
index 010348a..0020b37 100755
--- a/tests/cp/acl
+++ b/tests/cp/acl
@@ -36,28 +36,31 @@ grep '^#define USE_ACL 1' $CONFIG_HEADER > /dev/null ||
 mkdir -p a b || framework_failure
 touch a/file || framework_failure
 
-skip=no
 # Ensure that setfacl and getfacl work on this file system.
+skip=no
+acl1=`cd a && getfacl file` || skip=yes
 setfacl -m user:bin:rw a/file 2> /dev/null || skip=yes
-acl1=`cd a && getfacl file | grep -v ':bin:' | grep -v 'mask::'` \
-  || skip=yes
-
 test $skip = yes &&
   skip_test_ "'.' is not on a suitable file system for this test"
 
 # copy a file without preserving permissions
 cp a/file b/ || fail=1
-
 acl2=`cd b && getfacl file` || framework_failure
 test "$acl1" = "$acl2" || fail=1
-rm a/file || framework_failure
 
 # copy a file, preserving permissions
-touch a/file || framework_failure
-setfacl -m user:bin:rw a/file || framework_failure
 acl1=`cd a && getfacl file` || framework_failure
 cp -p a/file b/ || fail=1
 acl2=`cd b && getfacl file` || framework_failure
 test "$acl1" = "$acl2" || fail=1
 
+# copy a file, preserving permissions, with --attributes-only
+echo > a/file #add some data
+test -s a/file || framework_failure
+acl1=`cd a && getfacl file` || framework_failure
+cp -p --attributes-only a/file b/ || fail=1
+acl2=`cd b && getfacl file` || framework_failure
+test "$acl1" = "$acl2" || fail=1
+test -s b/file && fail=1
+
 Exit $fail
diff --git a/tests/cp/reflink-perm b/tests/cp/reflink-perm
index 92cb7ae..ff2e370 100755
--- a/tests/cp/reflink-perm
+++ b/tests/cp/reflink-perm
@@ -39,4 +39,8 @@ test "$mode" = "-rwxrwxrwx" || fail=1
 
 test copy -nt file && fail=1
 
+echo > file2 #file with data
+cp --reflink=auto --preserve --attributes-only file2 empty_copy || fail=1
+test -s empty_copy && fail=1
+
 Exit $fail
-- 
1.6.2.5