diff -r 4be55684eefc Doc/library/shutil.rst --- a/Doc/library/shutil.rst Thu Apr 05 17:04:28 2012 +0300 +++ b/Doc/library/shutil.rst Thu Apr 12 17:55:31 2012 +0200 @@ -89,6 +89,16 @@ .. versionchanged:: 3.3 Added *symlinks* argument. +.. function:: copyxattr(src, dst[, namespaces=["user"]][, symlinks=False]) + + Copy extended file system attributes from *src* to *dst*. Existing + attributes in *dst* that aren't present in *src* are deleted, those existing + in both will be overwritten. + + .. versionadded:: 3.3 + + Availability: Linux + .. function:: copy(src, dst[, symlinks=False])) Copy the file *src* to the file or directory *dst*. If *dst* is a directory, a @@ -102,14 +112,14 @@ .. function:: copy2(src, dst[, symlinks=False]) - Similar to :func:`shutil.copy`, but metadata is copied as well -- in fact, - this is just :func:`shutil.copy` followed by :func:`copystat`. This is + Similar to :func:`shutil.copy`, but metadata is copied as well. This is similar to the Unix command :program:`cp -p`. If *symlinks* is true, symbolic links won't be followed but recreated instead -- this resembles GNU's :program:`cp -P`. .. versionchanged:: 3.3 - Added *symlinks* argument. + Added *symlinks* argument, try to copy extended file system attributes + too (currently Linux only). .. function:: ignore_patterns(\*patterns) diff -r 4be55684eefc Lib/shutil.py --- a/Lib/shutil.py Thu Apr 05 17:04:28 2012 +0300 +++ b/Lib/shutil.py Thu Apr 12 17:55:31 2012 +0200 @@ -35,9 +35,10 @@ "ExecError", "make_archive", "get_archive_formats", "register_archive_format", "unregister_archive_format", "get_unpack_formats", "register_unpack_format", - "unregister_unpack_format", "unpack_archive", - "ignore_patterns", "chown"] - # disk_usage is added later, if available on the platform + "unregister_unpack_format", "unpack_archive", "ignore_patterns", + "chown"] + # disk_usage and copyxattr are added later, if available on the + # platform class Error(EnvironmentError): pass @@ -164,6 +165,45 @@ why.errno != errno.EOPNOTSUPP): raise +if hasattr(os, 'listxattr'): + + __all__.append('copyxattr') + + def _filter_attrs_into_set(attrs, namespaces): + """Build set from attrs that are from desired namespaces.""" + return {attr for attr in attrs if attr.split('.')[0] in namespaces} + + def copyxattr(src, dst, namespaces=['user'], symlinks=False): + """Copy extended filesystem attributes from `src` to `dst`. + + Overwrite existing attributes and remove attributes that are missing in + `src`. + + Affect only attributes from specified namespace (just "user" by + default). + + If the optional flag `symlinks` is set, symlinks won't be followed. + + """ + if symlinks: + listxattr = os.llistxattr + removexattr = os.lremovexattr + setxattr = os.lsetxattr + getxattr = os.lgetxattr + else: + listxattr = os.listxattr + removexattr = os.removexattr + setxattr = os.setxattr + getxattr = os.getxattr + + src_attrs = _filter_attrs_into_set(listxattr(src), namespaces) + dst_attrs = _filter_attrs_into_set(listxattr(dst), namespaces) + + for attr in dst_attrs - src_attrs: + removexattr(dst, attr) + for attr in src_attrs: + setxattr(dst, attr, getxattr(src, attr)) + def copy(src, dst, symlinks=False): """Copy data and mode bits ("cp src dst"). @@ -191,6 +231,15 @@ dst = os.path.join(dst, os.path.basename(src)) copyfile(src, dst, symlinks=symlinks) copystat(src, dst, symlinks=symlinks) + if 'copyxattr' in __all__: + for ns in ['user', 'trusted', 'security', 'system']: + # Some namespaces may or may not work, depending on privileges + # and/or names. Also, not all file systems support extended + # attributes. + try: + copyxattr(src, dst, namespaces=ns, symlinks=symlinks) + except: + pass def ignore_patterns(*patterns): """Function that can be used as copytree() ignore parameter. diff -r 4be55684eefc Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py Thu Apr 05 17:04:28 2012 +0300 +++ b/Lib/test/test_shutil.py Thu Apr 12 17:55:31 2012 +0200 @@ -282,6 +282,66 @@ self.assertTrue(abs(os.stat(src).st_mtime - os.stat(dst).st_mtime) < 00000.1) + @unittest.skipUnless(hasattr(shutil, 'copyxattr'), 'requires copyxattr') + def test_filter_attrs_into_set(self): + l = ['a.1', 'b.2', 'c.3'] + self.assertEqual( + shutil._filter_attrs_into_set(l, ['a', 'c']), + {'a.1', 'c.3'}) + self.assertEqual( + shutil._filter_attrs_into_set(l, ['d']), + set()) + self.assertEqual( + shutil._filter_attrs_into_set(l, []), + set()) + + @unittest.skipUnless(hasattr(shutil, 'copyxattr'), 'requires copyxattr') + def test_copyxattr(self): + tmp_dir = self.mkdtemp() + src = os.path.join(tmp_dir, 'foo') + write_file(src, 'foo') + os.setxattr(src, b'user.foo', b'42') + ns = ['user'] + if hasattr(os, 'geteuid') and os.geteuid() == 0: + os.setxattr(src, b'trusted.baz', b'43') + os.setxattr(src, b'security.qux', b'44') + ns.append('security') + dst = os.path.join(tmp_dir, 'bar') + write_file(dst, 'bar') + os.setxattr(dst, b'user.bar', b'43') + shutil.copyxattr(src, dst, ns, False) + if hasattr(os, 'geteuid') and os.geteuid() == 0: + self.assertRaises(OSError, os.getxattr, dst, b'trusted.baz') + src_attrs = shutil._filter_attrs_into_set(os.listxattr(src), ns) + dst_attrs = shutil._filter_attrs_into_set(os.listxattr(dst), ns) + self.assertEqual(src_attrs, dst_attrs) + for attr in src_attrs: + self.assertEqual(os.getxattr(src, attr), os.getxattr(dst, attr)) + + @support.skip_unless_symlink + @unittest.skipUnless(hasattr(shutil, 'copyxattr'), 'requires copyxattr') + def test_copyxattr_symlinks(self): + # On Linux, it's only possible to access non-user xattr for symlinks; + # which in turn require root privileges. This test should be expanded + # as soon as other platforms gain support for extended attributes. + if bool(hasattr(os, 'geteuid') and os.geteuid() == 0): + tmp_dir = self.mkdtemp() + src = os.path.join(tmp_dir, 'foo') + src_link = os.path.join(tmp_dir, 'baz') + write_file(src, 'foo') + os.symlink(src, src_link) + os.setxattr(src, b'trusted.foo', b'42') + os.lsetxattr(src_link, b'trusted.foo', b'43') + dst = os.path.join(tmp_dir, 'bar') + dst_link = os.path.join(tmp_dir, 'qux') + write_file(dst, 'bar') + os.symlink(dst, dst_link) + shutil.copyxattr(src_link, dst_link, ['trusted'], symlinks=True) + self.assertEqual(os.lgetxattr(dst_link, b'trusted.foo'), b'43') + self.assertRaises(OSError, os.getxattr, dst, b'trusted.foo') + shutil.copyxattr(src_link, dst, ['trusted'], symlinks=True) + self.assertEqual(os.getxattr(dst, b'trusted.foo'), b'43') + @support.skip_unless_symlink def test_copy_symlinks(self): tmp_dir = self.mkdtemp() @@ -340,6 +400,31 @@ if hasattr(os, 'lchflags') and hasattr(src_link_stat, 'st_flags'): self.assertEqual(src_link_stat.st_flags, dst_stat.st_flags) + @unittest.skipUnless(hasattr(shutil, 'copyxattr'), 'requires copyxattr') + def test_copy2_xattr(self): + tmp_dir = self.mkdtemp() + src = os.path.join(tmp_dir, 'foo') + dst = os.path.join(tmp_dir, 'bar') + write_file(src, 'foo') + os.setxattr(src, b'user.foo', b'42') + shutil.copy2(src, dst) + self.assertEqual( + os.getxattr(src, b'user.foo'), + os.getxattr(dst, b'user.foo')) + os.remove(dst) + + ns_used = [] + + def _fake_copyxattr(src, dst, namespaces=['user'], symlinks=False): + ns_used.append(namespaces) + raise OSError('fake exception') + old_copyxattr = shutil.copyxattr + shutil.copyxattr = _fake_copyxattr + shutil.copy2(src, dst) + self.assertEqual(ns_used, ['user', 'trusted', 'security', 'system']) + shutil.copyxattr = old_copyxattr + + @support.skip_unless_symlink def test_copyfile_symlinks(self): tmp_dir = self.mkdtemp()