mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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:
parent
089835469d
commit
f09d184821
6 changed files with 40 additions and 29 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue