Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 424246f

Browse files
committed
Issue #14082: shutil.copy2() now copies extended attributes, if possible.
Patch by Hynek Schlawack.
1 parent 4d688e3 commit 424246f

6 files changed

Lines changed: 141 additions & 22 deletions

File tree

Doc/library/shutil.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ Directory and files operations
102102

103103
.. function:: copy2(src, dst[, symlinks=False])
104104

105-
Similar to :func:`shutil.copy`, but metadata is copied as well -- in fact,
106-
this is just :func:`shutil.copy` followed by :func:`copystat`. This is
105+
Similar to :func:`shutil.copy`, but metadata is copied as well. This is
107106
similar to the Unix command :program:`cp -p`. If *symlinks* is true,
108107
symbolic links won't be followed but recreated instead -- this resembles
109108
GNU's :program:`cp -P`.
110109

111110
.. versionchanged:: 3.3
112-
Added *symlinks* argument.
111+
Added *symlinks* argument, try to copy extended file system attributes
112+
too (currently Linux only).
113113

114114
.. function:: ignore_patterns(\*patterns)
115115

Lib/shutil.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,36 @@ def _nop(*args, ns=None):
166166
else:
167167
raise
168168

169+
if hasattr(os, 'listxattr'):
170+
def _copyxattr(src, dst, symlinks=False):
171+
"""Copy extended filesystem attributes from `src` to `dst`.
172+
173+
Overwrite existing attributes.
174+
175+
If the optional flag `symlinks` is set, symlinks won't be followed.
176+
177+
"""
178+
if symlinks:
179+
listxattr = os.llistxattr
180+
removexattr = os.lremovexattr
181+
setxattr = os.lsetxattr
182+
getxattr = os.lgetxattr
183+
else:
184+
listxattr = os.listxattr
185+
removexattr = os.removexattr
186+
setxattr = os.setxattr
187+
getxattr = os.getxattr
188+
189+
for attr in listxattr(src):
190+
try:
191+
setxattr(dst, attr, getxattr(src, attr))
192+
except OSError as e:
193+
if e.errno not in (errno.EPERM, errno.ENOTSUP, errno.ENODATA):
194+
raise
195+
else:
196+
def _copyxattr(*args, **kwargs):
197+
pass
198+
169199
def copy(src, dst, symlinks=False):
170200
"""Copy data and mode bits ("cp src dst").
171201
@@ -193,6 +223,7 @@ def copy2(src, dst, symlinks=False):
193223
dst = os.path.join(dst, os.path.basename(src))
194224
copyfile(src, dst, symlinks=symlinks)
195225
copystat(src, dst, symlinks=symlinks)
226+
_copyxattr(src, dst, symlinks=symlinks)
196227

197228
def ignore_patterns(*patterns):
198229
"""Function that can be used as copytree() ignore parameter.

Lib/test/support.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,6 +1696,35 @@ def skip_unless_symlink(test):
16961696
msg = "Requires functional symlink implementation"
16971697
return test if ok else unittest.skip(msg)(test)
16981698

1699+
_can_xattr = None
1700+
def can_xattr():
1701+
global _can_xattr
1702+
if _can_xattr is not None:
1703+
return _can_xattr
1704+
if not hasattr(os, "setxattr"):
1705+
can = False
1706+
else:
1707+
try:
1708+
with open(TESTFN, "wb") as fp:
1709+
try:
1710+
os.fsetxattr(fp.fileno(), b"user.test", b"")
1711+
# Kernels < 2.6.39 don't respect setxattr flags.
1712+
kernel_version = platform.release()
1713+
m = re.match("2.6.(\d{1,2})", kernel_version)
1714+
can = m is None or int(m.group(1)) >= 39
1715+
except OSError:
1716+
can = False
1717+
finally:
1718+
unlink(TESTFN)
1719+
_can_xattr = can
1720+
return can
1721+
1722+
def skip_unless_xattr(test):
1723+
"""Skip decorator for tests that require functional extended attributes"""
1724+
ok = can_xattr()
1725+
msg = "no non-broken extended attribute support"
1726+
return test if ok else unittest.skip(msg)(test)
1727+
16991728
def patch(test_instance, object_to_patch, attr_name, new_value):
17001729
"""Override 'object_to_patch'.'attr_name' with 'new_value'.
17011730

Lib/test/test_os.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,25 +1810,7 @@ def test_flags(self):
18101810
raise
18111811

18121812

1813-
def supports_extended_attributes():
1814-
if not hasattr(os, "setxattr"):
1815-
return False
1816-
try:
1817-
with open(support.TESTFN, "wb") as fp:
1818-
try:
1819-
os.fsetxattr(fp.fileno(), b"user.test", b"")
1820-
except OSError:
1821-
return False
1822-
finally:
1823-
support.unlink(support.TESTFN)
1824-
# Kernels < 2.6.39 don't respect setxattr flags.
1825-
kernel_version = platform.release()
1826-
m = re.match("2.6.(\d{1,2})", kernel_version)
1827-
return m is None or int(m.group(1)) >= 39
1828-
1829-
1830-
@unittest.skipUnless(supports_extended_attributes(),
1831-
"no non-broken extended attribute support")
1813+
@support.skip_unless_xattr
18321814
class ExtendedAttributeTests(unittest.TestCase):
18331815

18341816
def tearDown(self):

Lib/test/test_shutil.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,67 @@ def _chflags_raiser(path, flags):
311311
finally:
312312
os.chflags = old_chflags
313313

314+
@support.skip_unless_xattr
315+
def test_copyxattr(self):
316+
tmp_dir = self.mkdtemp()
317+
src = os.path.join(tmp_dir, 'foo')
318+
write_file(src, 'foo')
319+
dst = os.path.join(tmp_dir, 'bar')
320+
write_file(dst, 'bar')
321+
322+
# no xattr == no problem
323+
shutil._copyxattr(src, dst)
324+
# common case
325+
os.setxattr(src, 'user.foo', b'42')
326+
os.setxattr(src, 'user.bar', b'43')
327+
shutil._copyxattr(src, dst)
328+
self.assertEqual(os.listxattr(src), os.listxattr(dst))
329+
self.assertEqual(
330+
os.getxattr(src, 'user.foo'),
331+
os.getxattr(dst, 'user.foo'))
332+
# check errors don't affect other attrs
333+
os.remove(dst)
334+
write_file(dst, 'bar')
335+
os_error = OSError(errno.EPERM, 'EPERM')
336+
337+
def _raise_on_user_foo(fname, attr, val):
338+
if attr == 'user.foo':
339+
raise os_error
340+
else:
341+
orig_setxattr(fname, attr, val)
342+
try:
343+
orig_setxattr = os.setxattr
344+
os.setxattr = _raise_on_user_foo
345+
shutil._copyxattr(src, dst)
346+
self.assertEqual(['user.bar'], os.listxattr(dst))
347+
finally:
348+
os.setxattr = orig_setxattr
349+
350+
@support.skip_unless_symlink
351+
@support.skip_unless_xattr
352+
@unittest.skipUnless(hasattr(os, 'geteuid') and os.geteuid() == 0,
353+
'root privileges required')
354+
def test_copyxattr_symlinks(self):
355+
# On Linux, it's only possible to access non-user xattr for symlinks;
356+
# which in turn require root privileges. This test should be expanded
357+
# as soon as other platforms gain support for extended attributes.
358+
tmp_dir = self.mkdtemp()
359+
src = os.path.join(tmp_dir, 'foo')
360+
src_link = os.path.join(tmp_dir, 'baz')
361+
write_file(src, 'foo')
362+
os.symlink(src, src_link)
363+
os.setxattr(src, 'trusted.foo', b'42')
364+
os.lsetxattr(src_link, 'trusted.foo', b'43')
365+
dst = os.path.join(tmp_dir, 'bar')
366+
dst_link = os.path.join(tmp_dir, 'qux')
367+
write_file(dst, 'bar')
368+
os.symlink(dst, dst_link)
369+
shutil._copyxattr(src_link, dst_link, symlinks=True)
370+
self.assertEqual(os.lgetxattr(dst_link, 'trusted.foo'), b'43')
371+
self.assertRaises(OSError, os.getxattr, dst, 'trusted.foo')
372+
shutil._copyxattr(src_link, dst, symlinks=True)
373+
self.assertEqual(os.getxattr(dst, 'trusted.foo'), b'43')
374+
314375
@support.skip_unless_symlink
315376
def test_copy_symlinks(self):
316377
tmp_dir = self.mkdtemp()
@@ -369,6 +430,19 @@ def test_copy2_symlinks(self):
369430
if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'):
370431
self.assertEqual(src_link_stat.st_flags, dst_stat.st_flags)
371432

433+
@support.skip_unless_xattr
434+
def test_copy2_xattr(self):
435+
tmp_dir = self.mkdtemp()
436+
src = os.path.join(tmp_dir, 'foo')
437+
dst = os.path.join(tmp_dir, 'bar')
438+
write_file(src, 'foo')
439+
os.setxattr(src, 'user.foo', b'42')
440+
shutil.copy2(src, dst)
441+
self.assertEqual(
442+
os.getxattr(src, 'user.foo'),
443+
os.getxattr(dst, 'user.foo'))
444+
os.remove(dst)
445+
372446
@support.skip_unless_symlink
373447
def test_copyfile_symlinks(self):
374448
tmp_dir = self.mkdtemp()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Core and Builtins
2323
Library
2424
-------
2525

26+
- Issue #14082: shutil.copy2() now copies extended attributes, if possible.
27+
Patch by Hynek Schlawack.
28+
2629
- Issue #13959: Make importlib.abc.FileLoader.load_module()/get_filename() and
2730
importlib.machinery.ExtensionFileLoader.load_module() have their single
2831
argument be optional. Allows for the replacement (and thus deprecation) of

0 commit comments

Comments
 (0)