|
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
| 8 | +import fcntl |
8 | 9 | import sys |
9 | 10 | import stat |
10 | 11 | import fnmatch |
@@ -280,6 +281,11 @@ def _stat(fn): |
280 | 281 | def _islink(fn): |
281 | 282 | return fn.is_symlink() if isinstance(fn, os.DirEntry) else os.path.islink(fn) |
282 | 283 |
|
| 284 | +def _samefilef(fsrc, fdst): |
| 285 | + if os.path.abspath(fsrc.name) == os.path.abspath(fdst.name): |
| 286 | + return True |
| 287 | + return False |
| 288 | + |
283 | 289 | def copyfile(src, dst, *, follow_symlinks=True): |
284 | 290 | """Copy data from src to dst in the most efficient way possible. |
285 | 291 |
|
@@ -307,48 +313,121 @@ def copyfile(src, dst, *, follow_symlinks=True): |
307 | 313 | if _WINDOWS and i == 0: |
308 | 314 | file_size = st.st_size |
309 | 315 |
|
| 316 | + # XXX Is there a race issue here with os.path.islink() operating on a |
| 317 | + # path rather than a file descriptor |
310 | 318 | if not follow_symlinks and _islink(src): |
311 | 319 | os.symlink(os.readlink(src), dst) |
| 320 | + return dst |
| 321 | + |
| 322 | + # In Unix, we must open the files non-blocking initially as opening a |
| 323 | + # FIFO without data present in it will block. |
| 324 | + if os.name == 'posix': |
| 325 | + src_flags = os.O_RDONLY | os.O_NONBLOCK |
| 326 | + ### Do we need os.O_EXCL? |
| 327 | + dst_flags = os.O_CREAT | os.O_WRONLY | os.O_NONBLOCK |
312 | 328 | else: |
313 | | - with open(src, 'rb') as fsrc: |
| 329 | + src_flags = os.O_RDONLY |
| 330 | + dst_flags = os.O_WRONLY |
| 331 | + |
| 332 | + def _src_opener(path, flags): |
| 333 | + return os.open(path, flags) |
| 334 | + |
| 335 | + # dst's opener is more complex. We need to atomically check and open the |
| 336 | + # destination if it doesn't exist. If it does exist, we need to detect so |
| 337 | + # we don't destroy it later should we have to fail out for some reason. |
| 338 | + dst_was_created = False |
| 339 | + def _dst_opener(path, flags): |
| 340 | + nonlocal dst_was_created |
| 341 | + try: |
| 342 | + dst_was_created = True |
| 343 | + return os.open(path, flags | dst_flags | os.O_EXCL) |
| 344 | + except FileExistsError: |
| 345 | + dst_was_created = False |
314 | 346 | try: |
315 | | - with open(dst, 'wb') as fdst: |
316 | | - # macOS |
317 | | - if _HAS_FCOPYFILE: |
| 347 | + # Open the already existing file in as non-blocking |
| 348 | + ### We have O_CREATE and O_NONBLOCK in dst_flags. Is that intended? |
| 349 | + fd = os.open(path, flags | dst_flags) |
| 350 | + # If the destination is already a FIFO for some reason, we'll get an |
| 351 | + # OSError here as we've opened a FIFO for a non-blocking write with |
| 352 | + # no reader on the other end. |
| 353 | + except OSError as e: |
| 354 | + # Use errno to decide whether the OSError we caught is due to |
| 355 | + # the above reason or some other reason. For the above reason, |
| 356 | + # raise the appropriate special file error. Otherwise, we need |
| 357 | + # to re-raise the OSError |
| 358 | + if e.errno == errno.ENXIO: |
| 359 | + raise SpecialFileError("`%s` is a named pipe" % path) |
| 360 | + else: |
| 361 | + raise e |
| 362 | + # This check is required to catch the case where the destination |
| 363 | + # file is a FIFO that's we successfully opened non-blocking because |
| 364 | + # for some reason there was a reader listening preventing an ENXIO |
| 365 | + # situation. |
| 366 | + st = os.fstat(fd) |
| 367 | + if stat.S_ISFIFO(st.st_mode): |
| 368 | + raise SpecialFileError("`%s` is a named pipe" % path) |
| 369 | + return fd |
| 370 | + |
| 371 | + with open(src, 'rb', opener=_src_opener) as fsrc: |
| 372 | + try: |
| 373 | + with open(dst, 'wb', opener=_dst_opener) as fdst: |
| 374 | + file_size = 0 |
| 375 | + # file.name is not populated when using os.fdopen() |
| 376 | + st = os.fstat(fsrc.fileno()) |
| 377 | + if stat.S_ISFIFO(st.st_mode): |
| 378 | + # Don't unlink dst unless we created it above |
| 379 | + if dst_was_created: |
| 380 | + os.unlink(dst) |
| 381 | + raise SpecialFileError("`%s` is a named pipe" % src) |
| 382 | + if _WINDOWS and i == 0: |
| 383 | + file_size = st.st_size |
| 384 | + |
| 385 | + # In Unix, we must set the file descriptors back to blocking as that's |
| 386 | + # what the rest of this function expects. |
| 387 | + # Additonally, fsrc, and fdst must be turned into file objects |
| 388 | + if os.name == 'posix': |
| 389 | + srcfl = fcntl.fcntl(fsrc.fileno(), fcntl.F_GETFL) |
| 390 | + dstfl = fcntl.fcntl(fdst.fileno(), fcntl.F_GETFL) |
| 391 | + |
| 392 | + # Unset the non-blocking flag |
| 393 | + fcntl.fcntl(fsrc.fileno(), fcntl.F_SETFL, srcfl & ~os.O_NONBLOCK) |
| 394 | + fcntl.fcntl(fdst.fileno(), fcntl.F_SETFL, dstfl & ~os.O_NONBLOCK) |
| 395 | + |
| 396 | + # macOS |
| 397 | + if _HAS_FCOPYFILE: |
| 398 | + try: |
| 399 | + _fastcopy_fcopyfile(fsrc, fdst, posix._COPYFILE_DATA) |
| 400 | + return dst |
| 401 | + except _GiveupOnFastCopy: |
| 402 | + pass |
| 403 | + # Linux / Android / Solaris |
| 404 | + elif _USE_CP_SENDFILE or _USE_CP_COPY_FILE_RANGE: |
| 405 | + # reflink may be implicit in copy_file_range. |
| 406 | + if _USE_CP_COPY_FILE_RANGE: |
318 | 407 | try: |
319 | | - _fastcopy_fcopyfile(fsrc, fdst, posix._COPYFILE_DATA) |
| 408 | + _fastcopy_copy_file_range(fsrc, fdst) |
| 409 | + return dst |
| 410 | + except _GiveupOnFastCopy: |
| 411 | + pass |
| 412 | + if _USE_CP_SENDFILE: |
| 413 | + try: |
| 414 | + _fastcopy_sendfile(fsrc, fdst) |
320 | 415 | return dst |
321 | 416 | except _GiveupOnFastCopy: |
322 | 417 | pass |
323 | | - # Linux / Android / Solaris |
324 | | - elif _USE_CP_SENDFILE or _USE_CP_COPY_FILE_RANGE: |
325 | | - # reflink may be implicit in copy_file_range. |
326 | | - if _USE_CP_COPY_FILE_RANGE: |
327 | | - try: |
328 | | - _fastcopy_copy_file_range(fsrc, fdst) |
329 | | - return dst |
330 | | - except _GiveupOnFastCopy: |
331 | | - pass |
332 | | - if _USE_CP_SENDFILE: |
333 | | - try: |
334 | | - _fastcopy_sendfile(fsrc, fdst) |
335 | | - return dst |
336 | | - except _GiveupOnFastCopy: |
337 | | - pass |
338 | | - # Windows, see: |
339 | | - # https://github.com/python/cpython/pull/7160#discussion_r195405230 |
340 | | - elif _WINDOWS and file_size > 0: |
341 | | - _copyfileobj_readinto(fsrc, fdst, min(file_size, COPY_BUFSIZE)) |
342 | | - return dst |
343 | | - |
344 | | - copyfileobj(fsrc, fdst) |
345 | 418 |
|
346 | | - # Issue 43219, raise a less confusing exception |
347 | | - except IsADirectoryError as e: |
348 | | - if not os.path.exists(dst): |
349 | | - raise FileNotFoundError(f'Directory does not exist: {dst}') from e |
350 | | - else: |
351 | | - raise |
| 419 | + # Windows, see: |
| 420 | + # https://github.com/python/cpython/pull/7160#discussion_r195405230 |
| 421 | + elif _WINDOWS and file_size > 0: |
| 422 | + _copyfileobj_readinto(fsrc, fdst, min(file_size, COPY_BUFSIZE)) |
| 423 | + return dst |
| 424 | + copyfileobj(fsrc, fdst) |
| 425 | + |
| 426 | + # Issue 43219, raise a less confusing exception |
| 427 | + except IsADirectoryError as e: |
| 428 | + if not os.path.exists(dst): |
| 429 | + raise FileNotFoundError(f'Directory does not exist: {dst}') from e |
| 430 | + raise |
352 | 431 |
|
353 | 432 | return dst |
354 | 433 |
|
|
0 commit comments