#752892 coreutils: cp --update --archive unnecessarily deletes and re-creates hard links

Package:
coreutils
Source:
coreutils
Description:
GNU core utilities
Submitter:
Toomas Tamm
Date:
2014-06-27 14:15:07 UTC
Severity:
normal
#752892#5
Date:
2014-06-27 14:01:59 UTC
From:
To:
cp --update --archive can be used to make and maintain backup copies of directory
trees. I am using this in a script with --verbose (I post-process the output in
this script). After upgrading to wheezy (coreutils 8.20) from lenny (coreutils
6.10), the cp output has included a lot of lines like

removed ‘path/to/certain/file’

All the listed files have more than one hard link. The following test case illustrates
the behaviour:

$ mkdir x
$ date > x/a
$ ln x/a x/b
$ ls -l x
total 8
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 a
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 b
$ cp --update --archive --verbose  --no-target-directory ./x ./y
‘./x’ -> ‘./y’
‘./x/b’ -> ‘./y/b’
‘./x/a’ -> ‘./y/a’
$ ls -l y
total 8
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 a
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 b
$ # The following command is the one which is showing the problem:
$ cp --update --archive --verbose  --no-target-directory ./x ./y
removed ‘./y/a’
$ # The following listing shows that the link was actually re-created:
$ ls -l y
total 8
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 a
-rw-rw-r-- 2 toomas toomas 30 Jun 27 16:36 b

Doing a "strace" on the last "cp" reveals the following sequence of events:

lstat("./x/b", {st_mode=S_IFREG|0664, st_size=30, ...}) = 0
stat("./y/b", {st_mode=S_IFREG|0664, st_size=30, ...}) = 0
lstat("./x/a", {st_mode=S_IFREG|0664, st_size=30, ...}) = 0
stat("./y/a", {st_mode=S_IFREG|0664, st_size=30, ...}) = 0
linkat(AT_FDCWD, "./y/b", AT_FDCWD, "./y/a", 0) = -1 EEXIST (File exists)
unlink("./y/a")                         = 0
linkat(AT_FDCWD, "./y/b", AT_FDCWD, "./y/a", 0) = 0

So even though the program has stat()ed both y/a and y/b and should "know"
that they already have identical inode numbers, is still tries to link y/a
to y/b, fails, removes y/a, and re-creates the hard link again.

With large directory trees containing numerous hard links this seems like
an unnecessary waste of resources, in addition to creating the initially
very confusing "removed ‘path/to/certain/file’" messages on the verbose
output.

In my opinion, the correct behaviour should omit the linkat(), unlink(),
linkat() sequence after comparing the inode numbers of existing files.

Toomas Tamm