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:
Barney Gale 2024-06-19 01:59:54 +01:00 committed by GitHub
parent 9f741e55c1
commit 20d5b84f57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 86 additions and 11 deletions

View file

@ -4,6 +4,7 @@
from errno import EBADF, EOPNOTSUPP, ETXTBSY, EXDEV
import os
import stat
import sys
try:
import fcntl
@ -91,12 +92,32 @@ def copyfd(source_fd, target_fd):
copyfd = None
if _winapi and hasattr(_winapi, 'CopyFile2'):
def copyfile(source, target):
if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_file_attributes'):
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).
"""
_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:
copyfile = None