mirror of https://github.com/python/cpython
GH-73991: Add follow_symlinks argument to `pathlib.Path.copy()` (#120519)
Add support for not following symlinks in `pathlib.Path.copy()`. On Windows we add the `COPY_FILE_COPY_SYMLINK` flag is following symlinks is disabled. If the source is symlink to a directory, this call will fail with `ERROR_ACCESS_DENIED`. In this case we add `COPY_FILE_DIRECTORY` to the flags and retry. This can fail on old Windowses, which we note in the docs. No news as `copy()` was only just added.
This commit is contained in:
parent
9f741e55c1
commit
20d5b84f57
|
@ -1432,17 +1432,26 @@ Creating files and directories
|
||||||
Copying, renaming and deleting
|
Copying, renaming and deleting
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. method:: Path.copy(target)
|
.. method:: Path.copy(target, *, follow_symlinks=True)
|
||||||
|
|
||||||
Copy the contents of this file to the *target* file. If *target* specifies
|
Copy the contents of this file to the *target* file. If *target* specifies
|
||||||
a file that already exists, it will be replaced.
|
a file that already exists, it will be replaced.
|
||||||
|
|
||||||
|
If *follow_symlinks* is false, and this file is a symbolic link, *target*
|
||||||
|
will be created as a symbolic link. If *follow_symlinks* is true and this
|
||||||
|
file is a symbolic link, *target* will be a copy of the symlink target.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This method uses operating system functionality to copy file content
|
This method uses operating system functionality to copy file content
|
||||||
efficiently. The OS might also copy some metadata, such as file
|
efficiently. The OS might also copy some metadata, such as file
|
||||||
permissions. After the copy is complete, users may wish to call
|
permissions. After the copy is complete, users may wish to call
|
||||||
:meth:`Path.chmod` to set the permissions of the target file.
|
:meth:`Path.chmod` to set the permissions of the target file.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
On old builds of Windows (before Windows 10 build 19041), this method
|
||||||
|
raises :exc:`OSError` when a symlink to a directory is encountered and
|
||||||
|
*follow_symlinks* is false.
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -790,14 +790,19 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
raise UnsupportedOperation(self._unsupported_msg('mkdir()'))
|
raise UnsupportedOperation(self._unsupported_msg('mkdir()'))
|
||||||
|
|
||||||
def copy(self, target):
|
def copy(self, target, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
Copy the contents of this file to the given target.
|
Copy the contents of this file to the given target. If this file is a
|
||||||
|
symlink and follow_symlinks is false, a symlink will be created at the
|
||||||
|
target.
|
||||||
"""
|
"""
|
||||||
if not isinstance(target, PathBase):
|
if not isinstance(target, PathBase):
|
||||||
target = self.with_segments(target)
|
target = self.with_segments(target)
|
||||||
if self._samefile_safe(target):
|
if self._samefile_safe(target):
|
||||||
raise OSError(f"{self!r} and {target!r} are the same file")
|
raise OSError(f"{self!r} and {target!r} are the same file")
|
||||||
|
if not follow_symlinks and self.is_symlink():
|
||||||
|
target.symlink_to(self.readlink())
|
||||||
|
return
|
||||||
with self.open('rb') as source_f:
|
with self.open('rb') as source_f:
|
||||||
try:
|
try:
|
||||||
with target.open('wb') as target_f:
|
with target.open('wb') as target_f:
|
||||||
|
|
|
@ -782,9 +782,11 @@ class Path(PathBase, PurePath):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if copyfile:
|
if copyfile:
|
||||||
def copy(self, target):
|
def copy(self, target, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
Copy the contents of this file to the given target.
|
Copy the contents of this file to the given target. If this file is a
|
||||||
|
symlink and follow_symlinks is false, a symlink will be created at the
|
||||||
|
target.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
target = os.fspath(target)
|
target = os.fspath(target)
|
||||||
|
@ -792,9 +794,9 @@ class Path(PathBase, PurePath):
|
||||||
if isinstance(target, PathBase):
|
if isinstance(target, PathBase):
|
||||||
# Target is an instance of PathBase but not os.PathLike.
|
# Target is an instance of PathBase but not os.PathLike.
|
||||||
# Use generic implementation from PathBase.
|
# Use generic implementation from PathBase.
|
||||||
return PathBase.copy(self, target)
|
return PathBase.copy(self, target, follow_symlinks=follow_symlinks)
|
||||||
raise
|
raise
|
||||||
copyfile(os.fspath(self), target)
|
copyfile(os.fspath(self), target, follow_symlinks)
|
||||||
|
|
||||||
def chmod(self, mode, *, follow_symlinks=True):
|
def chmod(self, mode, *, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -4,6 +4,7 @@ Low-level OS functionality wrappers used by pathlib.
|
||||||
|
|
||||||
from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
|
from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
import sys
|
import sys
|
||||||
try:
|
try:
|
||||||
import fcntl
|
import fcntl
|
||||||
|
@ -91,12 +92,32 @@ else:
|
||||||
copyfd = None
|
copyfd = None
|
||||||
|
|
||||||
|
|
||||||
if _winapi and hasattr(_winapi, 'CopyFile2'):
|
if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_file_attributes'):
|
||||||
def copyfile(source, target):
|
def _is_dirlink(path):
|
||||||
|
try:
|
||||||
|
st = os.lstat(path)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return False
|
||||||
|
return (st.st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY and
|
||||||
|
st.st_reparse_tag == stat.IO_REPARSE_TAG_SYMLINK)
|
||||||
|
|
||||||
|
def copyfile(source, target, follow_symlinks):
|
||||||
"""
|
"""
|
||||||
Copy from one file to another using CopyFile2 (Windows only).
|
Copy from one file to another using CopyFile2 (Windows only).
|
||||||
"""
|
"""
|
||||||
_winapi.CopyFile2(source, target, 0)
|
if follow_symlinks:
|
||||||
|
flags = 0
|
||||||
|
else:
|
||||||
|
flags = _winapi.COPY_FILE_COPY_SYMLINK
|
||||||
|
try:
|
||||||
|
_winapi.CopyFile2(source, target, flags)
|
||||||
|
return
|
||||||
|
except OSError as err:
|
||||||
|
# Check for ERROR_ACCESS_DENIED
|
||||||
|
if err.winerror != 5 or not _is_dirlink(source):
|
||||||
|
raise
|
||||||
|
flags |= _winapi.COPY_FILE_DIRECTORY
|
||||||
|
_winapi.CopyFile2(source, target, flags)
|
||||||
else:
|
else:
|
||||||
copyfile = None
|
copyfile = None
|
||||||
|
|
||||||
|
|
|
@ -1743,7 +1743,7 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
source.copy(target)
|
source.copy(target)
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_copy_symlink(self):
|
def test_copy_symlink_follow_symlinks_true(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'linkA'
|
source = base / 'linkA'
|
||||||
target = base / 'copyA'
|
target = base / 'copyA'
|
||||||
|
@ -1752,6 +1752,26 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertFalse(target.is_symlink())
|
self.assertFalse(target.is_symlink())
|
||||||
self.assertEqual(source.read_text(), target.read_text())
|
self.assertEqual(source.read_text(), target.read_text())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_symlink_follow_symlinks_false(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkA'
|
||||||
|
target = base / 'copyA'
|
||||||
|
source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_directory_symlink_follow_symlinks_false(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'linkB'
|
||||||
|
target = base / 'copyA'
|
||||||
|
source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertEqual(source.readlink(), target.readlink())
|
||||||
|
|
||||||
def test_copy_to_existing_file(self):
|
def test_copy_to_existing_file(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'fileA'
|
source = base / 'fileA'
|
||||||
|
@ -1780,6 +1800,19 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertFalse(real_target.is_symlink())
|
self.assertFalse(real_target.is_symlink())
|
||||||
self.assertEqual(source.read_text(), real_target.read_text())
|
self.assertEqual(source.read_text(), real_target.read_text())
|
||||||
|
|
||||||
|
@needs_symlinks
|
||||||
|
def test_copy_to_existing_symlink_follow_symlinks_false(self):
|
||||||
|
base = self.cls(self.base)
|
||||||
|
source = base / 'dirB' / 'fileB'
|
||||||
|
target = base / 'linkA'
|
||||||
|
real_target = base / 'fileA'
|
||||||
|
source.copy(target, follow_symlinks=False)
|
||||||
|
self.assertTrue(target.exists())
|
||||||
|
self.assertTrue(target.is_symlink())
|
||||||
|
self.assertTrue(real_target.exists())
|
||||||
|
self.assertFalse(real_target.is_symlink())
|
||||||
|
self.assertEqual(source.read_text(), real_target.read_text())
|
||||||
|
|
||||||
def test_copy_empty(self):
|
def test_copy_empty(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
source = base / 'empty'
|
source = base / 'empty'
|
||||||
|
|
|
@ -3166,6 +3166,11 @@ static int winapi_exec(PyObject *m)
|
||||||
#define COPY_FILE_REQUEST_COMPRESSED_TRAFFIC 0x10000000
|
#define COPY_FILE_REQUEST_COMPRESSED_TRAFFIC 0x10000000
|
||||||
#endif
|
#endif
|
||||||
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_COMPRESSED_TRAFFIC);
|
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_COMPRESSED_TRAFFIC);
|
||||||
|
#ifndef COPY_FILE_DIRECTORY
|
||||||
|
// Only defined in newer WinSDKs
|
||||||
|
#define COPY_FILE_DIRECTORY 0x00000080
|
||||||
|
#endif
|
||||||
|
WINAPI_CONSTANT(F_DWORD, COPY_FILE_DIRECTORY);
|
||||||
|
|
||||||
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_STARTED);
|
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_STARTED);
|
||||||
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_FINISHED);
|
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_FINISHED);
|
||||||
|
|
Loading…
Reference in New Issue