|
14 | 14 | import logging |
15 | 15 | import os |
16 | 16 | import platform |
| 17 | +import shutil |
17 | 18 | import stat |
18 | 19 | import sys |
19 | 20 | import typing |
|
28 | 29 | except ImportError: # pragma: no cover |
29 | 30 | scandir = None |
30 | 31 |
|
| 32 | +try: |
| 33 | + from os import sendfile |
| 34 | +except ImportError: |
| 35 | + try: |
| 36 | + from sendfile import sendfile |
| 37 | + except ImportError: |
| 38 | + sendfile = None |
| 39 | + |
31 | 40 | from . import errors |
32 | 41 | from .errors import FileExists |
33 | 42 | from .base import FS |
34 | 43 | from .enums import ResourceType |
35 | 44 | from ._fscompat import fsencode, fsdecode, fspath |
36 | 45 | from .info import Info |
37 | | -from .path import basename |
| 46 | +from .path import basename, dirname |
38 | 47 | from .permissions import Permissions |
39 | 48 | from .error_tools import convert_os_errors |
40 | 49 | from .mode import Mode, validate_open_mode |
@@ -361,90 +370,79 @@ def removedir(self, path): |
361 | 370 | # Optional Methods |
362 | 371 | # -------------------------------------------------------- |
363 | 372 |
|
| 373 | + # --- Type hint for opendir ------------------------------ |
| 374 | + |
364 | 375 | if False: # typing.TYPE_CHECKING |
365 | 376 |
|
366 | 377 | def opendir(self, path, factory=None): |
367 | 378 | # type: (_O, Text, Optional[_OpendirFactory]) -> SubFS[_O] |
368 | 379 | pass |
369 | 380 |
|
370 | | - def getsyspath(self, path): |
371 | | - # type: (Text) -> Text |
372 | | - sys_path = os.path.join(self._root_path, path.lstrip("/").replace("/", os.sep)) |
373 | | - return sys_path |
374 | | - |
375 | | - def geturl(self, path, purpose="download"): |
376 | | - # type: (Text, Text) -> Text |
377 | | - if purpose != "download": |
378 | | - raise NoURL(path, purpose) |
379 | | - return "file://" + self.getsyspath(path) |
380 | 381 |
|
381 | | - def gettype(self, path): |
382 | | - # type: (Text) -> ResourceType |
383 | | - self.check() |
384 | | - sys_path = self._to_sys_path(path) |
385 | | - with convert_os_errors("gettype", path): |
386 | | - stat = os.stat(sys_path) |
387 | | - resource_type = self._get_type_from_stat(stat) |
388 | | - return resource_type |
| 382 | + # --- Backport of os.sendfile for Python < 3.8 ----------- |
| 383 | + |
| 384 | + def _check_copy(self, src_path, dst_path, overwrite=False): |
| 385 | + # validate individual paths |
| 386 | + _src_path = self.validatepath(src_path) |
| 387 | + _dst_path = self.validatepath(dst_path) |
| 388 | + # check src_path exists and is a file |
| 389 | + if self.gettype(src_path) is not ResourceType.file: |
| 390 | + raise errors.FileExpected(src_path) |
| 391 | + # check dst_path does not exist if we are not overwriting |
| 392 | + if not overwrite and self.exists(_dst_path): |
| 393 | + raise errors.DestinationExists(dst_path) |
| 394 | + # check parent dir of _dst_path exists and is a directory |
| 395 | + if self.gettype(dirname(dst_path)) is not ResourceType.directory: |
| 396 | + raise errors.DirectoryExpected(dirname(dst_path)) |
| 397 | + return _src_path, _dst_path |
| 398 | + |
| 399 | + |
| 400 | + if sys.version_info[:2] < (3, 8) and sendfile is not None: |
| 401 | + |
| 402 | + _sendfile_error_codes = frozenset({ |
| 403 | + errno.EIO, |
| 404 | + errno.EINVAL, |
| 405 | + errno.ENOSYS, |
| 406 | + errno.ENOTSUP, |
| 407 | + errno.EBADF, |
| 408 | + errno.ENOTSOCK, |
| 409 | + errno.EOPNOTSUPP, |
| 410 | + }) |
| 411 | + |
| 412 | + def copy(self, src_path, dst_path, overwrite=False): |
| 413 | + # type: (Text, Text, bool) -> None |
| 414 | + with self._lock: |
| 415 | + # validate and canonicalise paths |
| 416 | + _src_path, _dst_path = self._check_copy(src_path, dst_path, overwrite) |
| 417 | + _src_sys, _dst_sys = self.getsyspath(_src_path), self.getsyspath(_dst_path) |
| 418 | + # attempt using sendfile |
| 419 | + try: |
| 420 | + # initialise variables to pass to sendfile |
| 421 | + # open files to obtain a file descriptor |
| 422 | + with io.open(_src_sys, 'r') as src: |
| 423 | + with io.open(_dst_sys, 'w') as dst: |
| 424 | + fd_src, fd_dst = src.fileno(), dst.fileno() |
| 425 | + sent = maxsize = os.fstat(fd_src).st_size |
| 426 | + offset = 0 |
| 427 | + while sent > 0: |
| 428 | + sent = sendfile(fd_dst, fd_src, offset, maxsize) |
| 429 | + offset += sent |
| 430 | + except OSError as e: |
| 431 | + # the error is not a simple "sendfile not supported" error |
| 432 | + if e.errno not in self._sendfile_error_codes: |
| 433 | + raise |
| 434 | + # fallback using the shutil implementation |
| 435 | + shutil.copy2(_src_sys, _dst_sys) |
389 | 436 |
|
390 | | - def islink(self, path): |
391 | | - # type: (Text) -> bool |
392 | | - self.check() |
393 | | - _path = self.validatepath(path) |
394 | | - sys_path = self._to_sys_path(_path) |
395 | | - if not self.exists(path): |
396 | | - raise errors.ResourceNotFound(path) |
397 | | - with convert_os_errors("islink", path): |
398 | | - return os.path.islink(sys_path) |
| 437 | + else: |
399 | 438 |
|
400 | | - def open( |
401 | | - self, |
402 | | - path, # type: Text |
403 | | - mode="r", # type: Text |
404 | | - buffering=-1, # type: int |
405 | | - encoding=None, # type: Optional[Text] |
406 | | - errors=None, # type: Optional[Text] |
407 | | - newline="", # type: Text |
408 | | - line_buffering=False, # type: bool |
409 | | - **options # type: Any |
410 | | - ): |
411 | | - # type: (...) -> IO |
412 | | - _mode = Mode(mode) |
413 | | - validate_open_mode(mode) |
414 | | - self.check() |
415 | | - _path = self.validatepath(path) |
416 | | - sys_path = self._to_sys_path(_path) |
417 | | - with convert_os_errors("open", path): |
418 | | - if six.PY2 and _mode.exclusive: |
419 | | - sys_path = os.open(sys_path, os.O_RDWR | os.O_CREAT | os.O_EXCL) |
420 | | - _encoding = encoding or "utf-8" |
421 | | - return io.open( |
422 | | - sys_path, |
423 | | - mode=_mode.to_platform(), |
424 | | - buffering=buffering, |
425 | | - encoding=None if _mode.binary else _encoding, |
426 | | - errors=errors, |
427 | | - newline=None if _mode.binary else newline, |
428 | | - **options |
429 | | - ) |
| 439 | + def copy(self, src_path, dst_path, overwrite=False): |
| 440 | + # type: (Text, Text, bool) -> None |
| 441 | + with self._lock: |
| 442 | + _src_path, _dst_path = self._check_copy(src_path, dst_path, overwrite) |
| 443 | + shutil.copy2(self.getsyspath(_src_path), self.getsyspath(_dst_path)) |
430 | 444 |
|
431 | | - def setinfo(self, path, info): |
432 | | - # type: (Text, RawInfo) -> None |
433 | | - self.check() |
434 | | - _path = self.validatepath(path) |
435 | | - sys_path = self._to_sys_path(_path) |
436 | | - if not os.path.exists(sys_path): |
437 | | - raise errors.ResourceNotFound(path) |
438 | | - if "details" in info: |
439 | | - details = info["details"] |
440 | | - if "accessed" in details or "modified" in details: |
441 | | - _accessed = typing.cast(int, details.get("accessed")) |
442 | | - _modified = typing.cast(int, details.get("modified", _accessed)) |
443 | | - accessed = int(_modified if _accessed is None else _accessed) |
444 | | - modified = int(_modified) |
445 | | - if accessed is not None or modified is not None: |
446 | | - with convert_os_errors("setinfo", path): |
447 | | - os.utime(sys_path, (accessed, modified)) |
| 445 | + # --- Backport of os.scandir for Python < 3.5 ------------ |
448 | 446 |
|
449 | 447 | if scandir: |
450 | 448 |
|
@@ -545,3 +543,84 @@ def scandir( |
545 | 543 | start, end = page |
546 | 544 | iter_info = itertools.islice(iter_info, start, end) |
547 | 545 | return iter_info |
| 546 | + |
| 547 | + # --- Miscellaneous -------------------------------------- |
| 548 | + |
| 549 | + def getsyspath(self, path): |
| 550 | + # type: (Text) -> Text |
| 551 | + sys_path = os.path.join(self._root_path, path.lstrip("/").replace("/", os.sep)) |
| 552 | + return sys_path |
| 553 | + |
| 554 | + def geturl(self, path, purpose="download"): |
| 555 | + # type: (Text, Text) -> Text |
| 556 | + if purpose != "download": |
| 557 | + raise NoURL(path, purpose) |
| 558 | + return "file://" + self.getsyspath(path) |
| 559 | + |
| 560 | + def gettype(self, path): |
| 561 | + # type: (Text) -> ResourceType |
| 562 | + self.check() |
| 563 | + sys_path = self._to_sys_path(path) |
| 564 | + with convert_os_errors("gettype", path): |
| 565 | + stat = os.stat(sys_path) |
| 566 | + resource_type = self._get_type_from_stat(stat) |
| 567 | + return resource_type |
| 568 | + |
| 569 | + def islink(self, path): |
| 570 | + # type: (Text) -> bool |
| 571 | + self.check() |
| 572 | + _path = self.validatepath(path) |
| 573 | + sys_path = self._to_sys_path(_path) |
| 574 | + if not self.exists(path): |
| 575 | + raise errors.ResourceNotFound(path) |
| 576 | + with convert_os_errors("islink", path): |
| 577 | + return os.path.islink(sys_path) |
| 578 | + |
| 579 | + def open( |
| 580 | + self, |
| 581 | + path, # type: Text |
| 582 | + mode="r", # type: Text |
| 583 | + buffering=-1, # type: int |
| 584 | + encoding=None, # type: Optional[Text] |
| 585 | + errors=None, # type: Optional[Text] |
| 586 | + newline="", # type: Text |
| 587 | + line_buffering=False, # type: bool |
| 588 | + **options # type: Any |
| 589 | + ): |
| 590 | + # type: (...) -> IO |
| 591 | + _mode = Mode(mode) |
| 592 | + validate_open_mode(mode) |
| 593 | + self.check() |
| 594 | + _path = self.validatepath(path) |
| 595 | + sys_path = self._to_sys_path(_path) |
| 596 | + with convert_os_errors("open", path): |
| 597 | + if six.PY2 and _mode.exclusive: |
| 598 | + sys_path = os.open(sys_path, os.O_RDWR | os.O_CREAT | os.O_EXCL) |
| 599 | + _encoding = encoding or "utf-8" |
| 600 | + return io.open( |
| 601 | + sys_path, |
| 602 | + mode=_mode.to_platform(), |
| 603 | + buffering=buffering, |
| 604 | + encoding=None if _mode.binary else _encoding, |
| 605 | + errors=errors, |
| 606 | + newline=None if _mode.binary else newline, |
| 607 | + **options |
| 608 | + ) |
| 609 | + |
| 610 | + def setinfo(self, path, info): |
| 611 | + # type: (Text, RawInfo) -> None |
| 612 | + self.check() |
| 613 | + _path = self.validatepath(path) |
| 614 | + sys_path = self._to_sys_path(_path) |
| 615 | + if not os.path.exists(sys_path): |
| 616 | + raise errors.ResourceNotFound(path) |
| 617 | + if "details" in info: |
| 618 | + details = info["details"] |
| 619 | + if "accessed" in details or "modified" in details: |
| 620 | + _accessed = typing.cast(int, details.get("accessed")) |
| 621 | + _modified = typing.cast(int, details.get("modified", _accessed)) |
| 622 | + accessed = int(_modified if _accessed is None else _accessed) |
| 623 | + modified = int(_modified) |
| 624 | + if accessed is not None or modified is not None: |
| 625 | + with convert_os_errors("setinfo", path): |
| 626 | + os.utime(sys_path, (accessed, modified)) |
0 commit comments