Skip to content

Commit dffb615

Browse files
committed
pythongh-88745: Add _winapi.CopyFile2 and update shutil.copy2 to use it
1 parent bfd20d2 commit dffb615

File tree

8 files changed

+209
-1
lines changed

8 files changed

+209
-1
lines changed

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,14 @@ struct _Py_global_strings {
5858

5959
struct {
6060
STRUCT_FOR_ID(CANCELLED)
61+
STRUCT_FOR_ID(ExistingFileName)
6162
STRUCT_FOR_ID(FINISHED)
6263
STRUCT_FOR_ID(False)
64+
STRUCT_FOR_ID(Flags)
6365
STRUCT_FOR_ID(JSONDecodeError)
66+
STRUCT_FOR_ID(NewFileName)
6467
STRUCT_FOR_ID(PENDING)
68+
STRUCT_FOR_ID(ProgressRoutine)
6569
STRUCT_FOR_ID(Py_Repr)
6670
STRUCT_FOR_ID(TextIOWrapper)
6771
STRUCT_FOR_ID(True)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/shutil.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
if sys.platform == 'win32':
4444
import _winapi
45+
else:
46+
_winapi = None
4547

4648
COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024
4749
# This should never be removed, see rationale in:
@@ -435,6 +437,30 @@ def copy2(src, dst, *, follow_symlinks=True):
435437
"""
436438
if os.path.isdir(dst):
437439
dst = os.path.join(dst, os.path.basename(src))
440+
441+
if hasattr(_winapi, "CopyFile2"):
442+
src_ = os.fsdecode(src)
443+
dst_ = os.fsdecode(dst)
444+
flags = _winapi.COPY_FILE_ALLOW_DECRYPTED_DESTINATION # for compat
445+
if not follow_symlinks:
446+
flags |= _winapi.COPY_FILE_COPY_SYMLINK
447+
retry = False
448+
try:
449+
_winapi.CopyFile2(src_, dst_, flags)
450+
return dst
451+
except OSError as exc:
452+
if (exc.winerror == _winapi.ERROR_PRIVILEGE_NOT_HELD
453+
and not follow_symlinks):
454+
# Likely encountered a symlink we aren't allowed to create.
455+
# Fall back on the old code
456+
pass
457+
elif exc.winerror == _winapi.ERROR_ACCESS_DENIED:
458+
# Possibly encountered a hidden or readonly file we can't
459+
# overwrite. Fall back on old code
460+
pass
461+
else:
462+
raise
463+
438464
copyfile(src, dst, follow_symlinks=follow_symlinks)
439465
copystat(src, dst, follow_symlinks=follow_symlinks)
440466
return dst
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Improve performance of :func:`shutil.copy2` by using the operating system's
2+
``CopyFile2`` function. This may result in subtle changes to metadata copied
3+
along with some files, bringing them in line with normal OS behavior.

Modules/_winapi.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,7 @@ _winapi_GetFileType_impl(PyObject *module, HANDLE handle)
19471947
return result;
19481948
}
19491949

1950+
19501951
/*[clinic input]
19511952
_winapi._mimetypes_read_windows_registry
19521953
@@ -2075,6 +2076,59 @@ _winapi_NeedCurrentDirectoryForExePath_impl(PyObject *module,
20752076
return result;
20762077
}
20772078

2079+
2080+
/*[clinic input]
2081+
_winapi.CopyFile2
2082+
2083+
ExistingFileName: LPCWSTR
2084+
NewFileName: LPCWSTR
2085+
Flags: DWORD
2086+
ProgressRoutine: object = None
2087+
2088+
Copies a file from one name to a new name.
2089+
2090+
This is implemented using the CopyFile2 API, which preserves all stat
2091+
and metadata information apart from security attributes.
2092+
2093+
ProgressRoutine is reserved for future use, but is currently not
2094+
implemented. Its value is ignored.
2095+
[clinic start generated code]*/
2096+
2097+
static PyObject *
2098+
_winapi_CopyFile2_impl(PyObject *module, LPCWSTR ExistingFileName,
2099+
LPCWSTR NewFileName, DWORD Flags,
2100+
PyObject *ProgressRoutine)
2101+
/*[clinic end generated code: output=f3f588380da0000a input=13cf59b6e1a70fe6]*/
2102+
{
2103+
HRESULT hr;
2104+
COPYFILE2_EXTENDED_PARAMETERS params = { sizeof(COPYFILE2_EXTENDED_PARAMETERS) };
2105+
params.dwCopyFlags = Flags;
2106+
/* For future implementation. We ignore the value for now so that
2107+
users only have to test for 'CopyFile2' existing and not whether
2108+
the additional parameter exists.
2109+
if (ProgressRoutine != Py_None) {
2110+
params.pProgressRoutine = _winapi_CopyFile2ProgressRoutine;
2111+
params.pvCallbackContext = Py_NewRef(ProgressRoutine);
2112+
}
2113+
*/
2114+
Py_BEGIN_ALLOW_THREADS;
2115+
hr = CopyFile2(ExistingFileName, NewFileName, &params);
2116+
Py_END_ALLOW_THREADS;
2117+
/* For future implementation.
2118+
Py_XDECREF(ProgressRoutine);
2119+
*/
2120+
if (FAILED(hr)) {
2121+
if ((hr & 0xFFFF0000) == 0x80070000) {
2122+
PyErr_SetFromWindowsErr(hr & 0xFFFF);
2123+
} else {
2124+
PyErr_SetFromWindowsErr(hr);
2125+
}
2126+
return NULL;
2127+
}
2128+
Py_RETURN_NONE;
2129+
}
2130+
2131+
20782132
static PyMethodDef winapi_functions[] = {
20792133
_WINAPI_CLOSEHANDLE_METHODDEF
20802134
_WINAPI_CONNECTNAMEDPIPE_METHODDEF
@@ -2110,6 +2164,7 @@ static PyMethodDef winapi_functions[] = {
21102164
_WINAPI_GETFILETYPE_METHODDEF
21112165
_WINAPI__MIMETYPES_READ_WINDOWS_REGISTRY_METHODDEF
21122166
_WINAPI_NEEDCURRENTDIRECTORYFOREXEPATH_METHODDEF
2167+
_WINAPI_COPYFILE2_METHODDEF
21132168
{NULL, NULL}
21142169
};
21152170

@@ -2146,6 +2201,7 @@ static int winapi_exec(PyObject *m)
21462201
WINAPI_CONSTANT(F_DWORD, CREATE_NEW_PROCESS_GROUP);
21472202
WINAPI_CONSTANT(F_DWORD, DUPLICATE_SAME_ACCESS);
21482203
WINAPI_CONSTANT(F_DWORD, DUPLICATE_CLOSE_SOURCE);
2204+
WINAPI_CONSTANT(F_DWORD, ERROR_ACCESS_DENIED);
21492205
WINAPI_CONSTANT(F_DWORD, ERROR_ALREADY_EXISTS);
21502206
WINAPI_CONSTANT(F_DWORD, ERROR_BROKEN_PIPE);
21512207
WINAPI_CONSTANT(F_DWORD, ERROR_IO_PENDING);
@@ -2159,6 +2215,7 @@ static int winapi_exec(PyObject *m)
21592215
WINAPI_CONSTANT(F_DWORD, ERROR_OPERATION_ABORTED);
21602216
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_BUSY);
21612217
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_CONNECTED);
2218+
WINAPI_CONSTANT(F_DWORD, ERROR_PRIVILEGE_NOT_HELD);
21622219
WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
21632220
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_FIRST_PIPE_INSTANCE);
21642221
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_OVERLAPPED);
@@ -2252,6 +2309,34 @@ static int winapi_exec(PyObject *m)
22522309
WINAPI_CONSTANT(F_DWORD, LCMAP_TRADITIONAL_CHINESE);
22532310
WINAPI_CONSTANT(F_DWORD, LCMAP_UPPERCASE);
22542311

2312+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_ALLOW_DECRYPTED_DESTINATION);
2313+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_COPY_SYMLINK);
2314+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_FAIL_IF_EXISTS);
2315+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_NO_BUFFERING);
2316+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_NO_OFFLOAD);
2317+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_OPEN_SOURCE_FOR_WRITE);
2318+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_RESTARTABLE);
2319+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_SECURITY_PRIVILEGES);
2320+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_RESUME_FROM_PAUSE);
2321+
#ifndef COPY_FILE_REQUEST_COMPRESSED_TRAFFIC
2322+
// Only defined in newer WinSDKs
2323+
#define COPY_FILE_REQUEST_COMPRESSED_TRAFFIC 0x10000000
2324+
#endif
2325+
WINAPI_CONSTANT(F_DWORD, COPY_FILE_REQUEST_COMPRESSED_TRAFFIC);
2326+
2327+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_STARTED);
2328+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_CHUNK_FINISHED);
2329+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_STREAM_STARTED);
2330+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_STREAM_FINISHED);
2331+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_POLL_CONTINUE);
2332+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_CALLBACK_ERROR);
2333+
2334+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_CONTINUE);
2335+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_CANCEL);
2336+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_STOP);
2337+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_QUIET);
2338+
WINAPI_CONSTANT(F_DWORD, COPYFILE2_PROGRESS_PAUSE);
2339+
22552340
WINAPI_CONSTANT("i", NULL);
22562341

22572342
return 0;

Modules/clinic/_winapi.c.h

Lines changed: 71 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)