GH-73991: Support copying directory symlinks on older Windows (#120807)

Check for `ERROR_INVALID_PARAMETER` when calling `_winapi.CopyFile2()` and
raise `UnsupportedOperation`. In `Path.copy()`, handle this exception and
fall back to the `PathBase.copy()` implementation.
This commit is contained in:
Barney Gale 2024-07-03 04:30:29 +01:00 committed by GitHub
parent 089835469d
commit f09d184821
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 40 additions and 29 deletions

View file

@ -1554,11 +1554,6 @@ Copying, renaming and deleting
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

View file

@ -5,8 +5,8 @@ paths with operations that have semantics appropriate for different
operating systems. operating systems.
""" """
from ._abc import * from ._os import *
from ._local import * from ._local import *
__all__ = (_abc.__all__ + __all__ = (_os.__all__ +
_local.__all__) _local.__all__)

View file

@ -16,10 +16,7 @@ import operator
import posixpath import posixpath
from glob import _GlobberBase, _no_recurse_symlinks from glob import _GlobberBase, _no_recurse_symlinks
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
from ._os import copyfileobj from ._os import UnsupportedOperation, copyfileobj
__all__ = ["UnsupportedOperation"]
@functools.cache @functools.cache
@ -27,12 +24,6 @@ def _is_case_sensitive(parser):
return parser.normcase('Aa') == 'Aa' return parser.normcase('Aa') == 'Aa'
class UnsupportedOperation(NotImplementedError):
"""An exception that is raised when an unsupported operation is called on
a path object.
"""
pass
class ParserBase: class ParserBase:
"""Base class for path parsers, which do low-level path manipulation. """Base class for path parsers, which do low-level path manipulation.

View file

@ -17,8 +17,8 @@ try:
except ImportError: except ImportError:
grp = None grp = None
from ._abc import UnsupportedOperation, PurePathBase, PathBase from ._os import UnsupportedOperation, copyfile
from ._os import copyfile from ._abc import PurePathBase, PathBase
__all__ = [ __all__ = [
@ -791,12 +791,15 @@ class Path(PathBase, PurePath):
try: try:
target = os.fspath(target) target = os.fspath(target)
except TypeError: except TypeError:
if isinstance(target, PathBase): if not isinstance(target, PathBase):
# Target is an instance of PathBase but not os.PathLike. raise
# Use generic implementation from PathBase. else:
return PathBase.copy(self, target, follow_symlinks=follow_symlinks) try:
raise copyfile(os.fspath(self), target, follow_symlinks)
copyfile(os.fspath(self), target, follow_symlinks) return
except UnsupportedOperation:
pass # Fall through to generic code.
PathBase.copy(self, target, follow_symlinks=follow_symlinks)
def chmod(self, mode, *, follow_symlinks=True): def chmod(self, mode, *, follow_symlinks=True):
""" """

View file

@ -20,6 +20,15 @@ except ImportError:
_winapi = None _winapi = None
__all__ = ["UnsupportedOperation"]
class UnsupportedOperation(NotImplementedError):
"""An exception that is raised when an unsupported operation is attempted.
"""
pass
def get_copy_blocksize(infd): def get_copy_blocksize(infd):
"""Determine blocksize for fastcopying on Linux. """Determine blocksize for fastcopying on Linux.
Hopefully the whole file will be copied in a single call. Hopefully the whole file will be copied in a single call.
@ -106,18 +115,30 @@ if _winapi and hasattr(_winapi, 'CopyFile2') and hasattr(os.stat_result, 'st_fil
Copy from one file to another using CopyFile2 (Windows only). Copy from one file to another using CopyFile2 (Windows only).
""" """
if follow_symlinks: if follow_symlinks:
flags = 0 _winapi.CopyFile2(source, target, 0)
else: else:
# Use COPY_FILE_COPY_SYMLINK to copy a file symlink.
flags = _winapi.COPY_FILE_COPY_SYMLINK flags = _winapi.COPY_FILE_COPY_SYMLINK
try: try:
_winapi.CopyFile2(source, target, flags) _winapi.CopyFile2(source, target, flags)
return return
except OSError as err: except OSError as err:
# Check for ERROR_ACCESS_DENIED # Check for ERROR_ACCESS_DENIED
if err.winerror != 5 or not _is_dirlink(source): if err.winerror == 5 and _is_dirlink(source):
pass
else:
raise raise
# Add COPY_FILE_DIRECTORY to copy a directory symlink.
flags |= _winapi.COPY_FILE_DIRECTORY flags |= _winapi.COPY_FILE_DIRECTORY
_winapi.CopyFile2(source, target, flags) try:
_winapi.CopyFile2(source, target, flags)
except OSError as err:
# Check for ERROR_INVALID_PARAMETER
if err.winerror == 87:
raise UnsupportedOperation(err) from None
else:
raise
else: else:
copyfile = None copyfile = None

View file

@ -5,7 +5,8 @@ import errno
import stat import stat
import unittest import unittest
from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase from pathlib._os import UnsupportedOperation
from pathlib._abc import ParserBase, PurePathBase, PathBase
import posixpath import posixpath
from test.support import is_wasi from test.support import is_wasi