Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ def copyfile(src, dst, *, follow_symlinks=True):
symlink will be created instead of copying the file it points to.

"""
return _copyfile(src, dst, follow_symlinks, private=False)

def _copyfile(src, dst, follow_symlinks, *, private):
sys.audit("shutil.copyfile", src, dst)

if _samefile(src, dst):
Expand Down Expand Up @@ -328,7 +331,16 @@ def copyfile(src, dst, *, follow_symlinks=True):
else:
with open(src, 'rb') as fsrc:
try:
with open(dst, 'wb') as fdst:
if private:
def opener(path, flags):
# Only the owner can access a new file until copystat()
# applies the source mode. Setting xattrs needs write access.
mode = os.fstat(fsrc.fileno()).st_mode & stat.S_IRWXU
return os.open(path, flags, mode | stat.S_IWUSR)
fdst = open(dst, 'wb', opener=opener)
else:
fdst = open(dst, 'wb')
with fdst:
# macOS
if _HAS_FCOPYFILE:
try:
Expand Down Expand Up @@ -542,7 +554,7 @@ def copy2(src, dst, *, follow_symlinks=True):
else:
raise

copyfile(src, dst, follow_symlinks=follow_symlinks)
_copyfile(src, dst, follow_symlinks, private=os.name == 'posix')
copystat(src, dst, follow_symlinks=follow_symlinks)
return dst

Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1513,12 +1513,36 @@ def test_copy2_xattr(self):
dst = os.path.join(tmp_dir, 'bar')
create_file(src, 'foo')
os.setxattr(src, 'user.foo', b'42')
os.chmod(src, 0o444)
shutil.copy2(src, dst)
self.assertEqual(
os.getxattr(src, 'user.foo'),
os.getxattr(dst, 'user.foo'))
self.assertEqual(stat.S_IMODE(os.stat(dst).st_mode), 0o444)
os.remove(dst)

@unittest.skipUnless(hasattr(os, 'listxattr'), 'requires os.listxattr')
def test_copy2_listxattr_permission_error(self):
tmp_dir = self.mkdtemp()
src = os.path.join(tmp_dir, 'src')
create_file(src, 'contents')
for err, mode in ((errno.EACCES, 0o600), (errno.EPERM, 0o700)):
with self.subTest(errno=err, mode=mode), os_helper.temp_umask(0o022):
os.chmod(src, mode)
dst = os.path.join(tmp_dir, f'dst{err}')
error = PermissionError(err, os.strerror(err))

def deny_listxattr(*args, **kwargs):
self.assertEqual(stat.S_IMODE(os.stat(dst).st_mode), mode)
raise error

with unittest.mock.patch('os.listxattr', deny_listxattr):
with self.assertRaises(PermissionError) as cm:
shutil.copy2(src, dst)
self.assertIs(cm.exception, error)
self.assertEqual(stat.S_IMODE(os.stat(dst).st_mode), mode)
self.assertEqual(read_file(dst), 'contents')

def test_copy_return_value(self):
# copy and copy2 both return their destination path.
for fn in (shutil.copy, shutil.copy2):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
On POSIX, :func:`shutil.copy2` creates new files with owner-only permissions
until it applies the source metadata.
Loading