Skip to content

Commit

Permalink
pythongh-88745: Add _winapi.CopyFile2 and update shutil.copy2 to use it
Browse files Browse the repository at this point in the history
  • Loading branch information
zooba committed May 29, 2023
1 parent bfd20d2 commit dffb615
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,14 @@ struct _Py_global_strings {

struct {
STRUCT_FOR_ID(CANCELLED)
STRUCT_FOR_ID(ExistingFileName)
STRUCT_FOR_ID(FINISHED)
STRUCT_FOR_ID(False)
STRUCT_FOR_ID(Flags)
STRUCT_FOR_ID(JSONDecodeError)
STRUCT_FOR_ID(NewFileName)
STRUCT_FOR_ID(PENDING)
STRUCT_FOR_ID(ProgressRoutine)
STRUCT_FOR_ID(Py_Repr)
STRUCT_FOR_ID(TextIOWrapper)
STRUCT_FOR_ID(True)
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@

if sys.platform == 'win32':
import _winapi
else:
_winapi = None

COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
# This should never be removed, see rationale in:
Expand Down Expand Up @@ -435,6 +437,30 @@ def copy2(src, dst, *, follow_symlinks=True):
"""
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))

if hasattr(_winapi, "CopyFile2"):
src_ = os.fsdecode(src)
dst_ = os.fsdecode(dst)
flags = _winapi.COPY_FILE_ALLOW_DECRYPTED_DESTINATION # for compat
if not follow_symlinks:
flags |= _winapi.COPY_FILE_COPY_SYMLINK
retry = False
try:
_winapi.CopyFile2(src_, dst_, flags)
return dst
except OSError as exc:
if (exc.winerror == _winapi.ERROR_PRIVILEGE_NOT_HELD
and not follow_symlinks):
# Likely encountered a symlink we aren't allowed to create.
# Fall back on the old code
pass
elif exc.winerror == _winapi.ERROR_ACCESS_DENIED:
# Possibly encountered a hidden or readonly file we can't
# overwrite. Fall back on old code
pass
else:
raise

copyfile(src, dst, follow_symlinks=follow_symlinks)
copystat(src, dst, follow_symlinks=follow_symlinks)
return dst
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Improve performance of :func:`shutil.copy2` by using the operating system's
``CopyFile2`` function. This may result in subtle changes to metadata copied
along with some files, bringing them in line with normal OS behavior.
85 changes: 85 additions & 0 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,7 @@ _winapi_GetFileType_impl(PyObject *module, HANDLE handle)
return result;
}


/*[clinic input]
_winapi._mimetypes_read_windows_registry
Expand Down Expand Up @@ -2075,6 +2076,59 @@ _winapi_NeedCurrentDirectoryForExePath_impl(PyObject *module,
return result;
}


/*[clinic input]
_winapi.CopyFile2
ExistingFileName: LPCWSTR
NewFileName: LPCWSTR
Flags: DWORD
ProgressRoutine: object = None
Copies a file from one name to a new name.
This is implemented using the CopyFile2 API, which preserves all stat
and metadata information apart from security attributes.
ProgressRoutine is reserved for future use, but is currently not
implemented. Its value is ignored.
[clinic start generated code]*/

static PyObject *
_winapi_CopyFile2_impl(PyObject *module, LPCWSTR ExistingFileName,
LPCWSTR NewFileName, DWORD Flags,
PyObject *ProgressRoutine)
/*[clinic end generated code: output=f3f588380da0000a input=13cf59b6e1a70fe6]*/
{
HRESULT hr;
COPYFILE2_EXTENDED_PARAMETERS params = { sizeof(COPYFILE2_EXTENDED_PARAMETERS) };
params.dwCopyFlags = Flags;
/* For future implementation. We ignore the value for now so that
users only have to test for 'CopyFile2' existing and not whether
the additional parameter exists.
if (ProgressRoutine != Py_None) {
params.pProgressRoutine = _winapi_CopyFile2ProgressRoutine;
params.pvCallbackContext = Py_NewRef(ProgressRoutine);
}
*/
Py_BEGIN_ALLOW_THREADS;
hr = CopyFile2(ExistingFileName, NewFileName, &params);
Py_END_ALLOW_THREADS;
/* For future implementation.
Py_XDECREF(ProgressRoutine);
*/
if (FAILED(hr)) {
if ((hr & 0xFFFF0000) == 0x80070000) {
PyErr_SetFromWindowsErr(hr & 0xFFFF);
} else {
PyErr_SetFromWindowsErr(hr);
}
return NULL;
}
Py_RETURN_NONE;
}


static PyMethodDef winapi_functions[] = {
_WINAPI_CLOSEHANDLE_METHODDEF
_WINAPI_CONNECTNAMEDPIPE_METHODDEF
Expand Down Expand Up @@ -2110,6 +2164,7 @@ static PyMethodDef winapi_functions[] = {
_WINAPI_GETFILETYPE_METHODDEF
_WINAPI__MIMETYPES_READ_WINDOWS_REGISTRY_METHODDEF
_WINAPI_NEEDCURRENTDIRECTORYFOREXEPATH_METHODDEF
_WINAPI_COPYFILE2_METHODDEF
{NULL, NULL}
};

Expand Down Expand Up @@ -2146,6 +2201,7 @@ static int winapi_exec(PyObject *m)
WINAPI_CONSTANT(F_DWORD, CREATE_NEW_PROCESS_GROUP);
WINAPI_CONSTANT(F_DWORD, DUPLICATE_SAME_ACCESS);
WINAPI_CONSTANT(F_DWORD, DUPLICATE_CLOSE_SOURCE);
WINAPI_CONSTANT(F_DWORD, ERROR_ACCESS_DENIED);
WINAPI_CONSTANT(F_DWORD, ERROR_ALREADY_EXISTS);
WINAPI_CONSTANT(F_DWORD, ERROR_BROKEN_PIPE);
WINAPI_CONSTANT(F_DWORD, ERROR_IO_PENDING);
Expand All @@ -2159,6 +2215,7 @@ static int winapi_exec(PyObject *m)
WINAPI_CONSTANT(F_DWORD, ERROR_OPERATION_ABORTED);
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_BUSY);
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_CONNECTED);
WINAPI_CONSTANT(F_DWORD, ERROR_PRIVILEGE_NOT_HELD);
WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_FIRST_PIPE_INSTANCE);
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_OVERLAPPED);
Expand Down Expand Up @@ -2252,6 +2309,34 @@ static int winapi_exec(PyObject *m)
WINAPI_CONSTANT(F_DWORD, LCMAP_TRADITIONAL_CHINESE);
WINAPI_CONSTANT(F_DWORD, LCMAP_UPPERCASE);

WINAPI_CONSTANT(F_DWORD, COPY_FILE_ALLOW_DECRYPTED_DESTINATION);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_COPY_SYMLINK);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_FAIL_IF_EXISTS);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_NO_BUFFERING);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_NO_OFFLOAD);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_OPEN_SOURCE_FOR_WRITE);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_RESTARTABLE);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_SECURITY_PRIVILEGES);
WINAPI_CONSTANT(F_DWORD, COPY_FILE_RESUME_FROM_PAUSE);
#ifndef COPY_FILE_REQUEST_COMPRESSED_TRAFFIC
// Only defined in newer WinSDKs
#define COPY_FILE_REQUEST_COMPRESSED_TRAFFIC 0x10000000
#endif
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_COMPRESSED_TRAFFIC);

WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_STARTED);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_FINISHED);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_STREAM_STARTED);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_STREAM_FINISHED);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_POLL_CONTINUE);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_ERROR);

WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_CONTINUE);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_CANCEL);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_STOP);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_QUIET);
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_PAUSE);

WINAPI_CONSTANT("i", NULL);

return 0;
Expand Down
72 changes: 71 additions & 1 deletion Modules/clinic/_winapi.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit dffb615

Please sign in to comment.