1"""PyFilesystem base class. 2 3The filesystem base class is common to all filesystems. If you 4familiarize yourself with this (rather straightforward) API, you 5can work with any of the supported filesystems. 6 7""" 8 9from __future__ import absolute_import, print_function, unicode_literals 10 11import abc 12import hashlib 13import itertools 14import os 15import threading 16import time 17import typing 18from contextlib import closing 19from functools import partial, wraps 20import warnings 21 22import six 23 24from . import copy, errors, fsencode, iotools, move, tools, walk, wildcard 25from .glob import BoundGlobber 26from .mode import validate_open_mode 27from .path import abspath, join, normpath 28from .time import datetime_to_epoch 29from .walk import Walker 30 31if typing.TYPE_CHECKING: 32 from datetime import datetime 33 from threading import RLock 34 from typing import ( 35 Any, 36 BinaryIO, 37 Callable, 38 Collection, 39 Dict, 40 IO, 41 Iterable, 42 Iterator, 43 List, 44 Mapping, 45 Optional, 46 Text, 47 Tuple, 48 Type, 49 Union, 50 ) 51 from types import TracebackType 52 from .enums import ResourceType 53 from .info import Info, RawInfo 54 from .subfs import SubFS 55 from .permissions import Permissions 56 from .walk import BoundWalker 57 58 _F = typing.TypeVar("_F", bound="FS") 59 _T = typing.TypeVar("_T", bound="FS") 60 _OpendirFactory = Callable[[_T, Text], SubFS[_T]] 61 62 63__all__ = ["FS"] 64 65 66def _new_name(method, old_name): 67 """Return a method with a deprecation warning.""" 68 # Looks suspiciously like a decorator, but isn't! 69 70 @wraps(method) 71 def _method(*args, **kwargs): 72 warnings.warn( 73 "method '{}' has been deprecated, please rename to '{}'".format( 74 old_name, method.__name__ 75 ), 76 DeprecationWarning, 77 ) 78 return method(*args, **kwargs) 79 80 deprecated_msg = """ 81 Note: 82 .. deprecated:: 2.2.0 83 Please use `~{}` 84""".format( 85 method.__name__ 86 ) 87 if hasattr(_method, "__doc__"): 88 _method.__doc__ += deprecated_msg 89 90 return _method 91 92 93@six.add_metaclass(abc.ABCMeta) 94class FS(object): 95 """Base class for FS objects. 96 """ 97 98 # This is the "standard" meta namespace. 99 _meta = {} # type: Dict[Text, Union[Text, int, bool, None]] 100 101 # most FS will use default walking algorithms 102 walker_class = Walker 103 104 # default to SubFS, used by opendir and should be returned by makedir(s) 105 subfs_class = None 106 107 def __init__(self): 108 # type: (...) -> None 109 """Create a filesystem. See help(type(self)) for accurate signature. 110 """ 111 self._closed = False 112 self._lock = threading.RLock() 113 super(FS, self).__init__() 114 115 def __del__(self): 116 """Auto-close the filesystem on exit.""" 117 self.close() 118 119 def __enter__(self): 120 # type: (...) -> FS 121 """Allow use of filesystem as a context manager. 122 """ 123 return self 124 125 def __exit__( 126 self, 127 exc_type, # type: Optional[Type[BaseException]] 128 exc_value, # type: Optional[BaseException] 129 traceback, # type: Optional[TracebackType] 130 ): 131 # type: (...) -> None 132 """Close filesystem on exit. 133 """ 134 self.close() 135 136 @property 137 def glob(self): 138 """`~fs.glob.BoundGlobber`: a globber object.. 139 """ 140 return BoundGlobber(self) 141 142 @property 143 def walk(self): 144 # type: (_F) -> BoundWalker[_F] 145 """`~fs.walk.BoundWalker`: a walker bound to this filesystem. 146 """ 147 return self.walker_class.bind(self) 148 149 # ---------------------------------------------------------------- # 150 # Required methods # 151 # Filesystems must implement these methods. # 152 # ---------------------------------------------------------------- # 153 154 @abc.abstractmethod 155 def getinfo(self, path, namespaces=None): 156 # type: (Text, Optional[Collection[Text]]) -> Info 157 """Get information about a resource on a filesystem. 158 159 Arguments: 160 path (str): A path to a resource on the filesystem. 161 namespaces (list, optional): Info namespaces to query 162 (defaults to *[basic]*). 163 164 Returns: 165 ~fs.info.Info: resource information object. 166 167 For more information regarding resource information, see :ref:`info`. 168 169 """ 170 171 @abc.abstractmethod 172 def listdir(self, path): 173 # type: (Text) -> List[Text] 174 """Get a list of the resource names in a directory. 175 176 This method will return a list of the resources in a directory. 177 A *resource* is a file, directory, or one of the other types 178 defined in `~fs.enums.ResourceType`. 179 180 Arguments: 181 path (str): A path to a directory on the filesystem 182 183 Returns: 184 list: list of names, relative to ``path``. 185 186 Raises: 187 fs.errors.DirectoryExpected: If ``path`` is not a directory. 188 fs.errors.ResourceNotFound: If ``path`` does not exist. 189 190 """ 191 192 @abc.abstractmethod 193 def makedir( 194 self, 195 path, # type: Text 196 permissions=None, # type: Optional[Permissions] 197 recreate=False, # type: bool 198 ): 199 # type: (...) -> SubFS[FS] 200 """Make a directory. 201 202 Arguments: 203 path (str): Path to directory from root. 204 permissions (~fs.permissions.Permissions, optional): a 205 `Permissions` instance, or `None` to use default. 206 recreate (bool): Set to `True` to avoid raising an error if 207 the directory already exists (defaults to `False`). 208 209 Returns: 210 ~fs.subfs.SubFS: a filesystem whose root is the new directory. 211 212 Raises: 213 fs.errors.DirectoryExists: If the path already exists. 214 fs.errors.ResourceNotFound: If the path is not found. 215 216 """ 217 218 @abc.abstractmethod 219 def openbin( 220 self, 221 path, # type: Text 222 mode="r", # type: Text 223 buffering=-1, # type: int 224 **options # type: Any 225 ): 226 # type: (...) -> BinaryIO 227 """Open a binary file-like object. 228 229 Arguments: 230 path (str): A path on the filesystem. 231 mode (str): Mode to open file (must be a valid non-text mode, 232 defaults to *r*). Since this method only opens binary files, 233 the ``b`` in the mode string is implied. 234 buffering (int): Buffering policy (-1 to use default buffering, 235 0 to disable buffering, or any positive integer to indicate 236 a buffer size). 237 **options: keyword arguments for any additional information 238 required by the filesystem (if any). 239 240 Returns: 241 io.IOBase: a *file-like* object. 242 243 Raises: 244 fs.errors.FileExpected: If the path is not a file. 245 fs.errors.FileExists: If the file exists, and *exclusive mode* 246 is specified (``x`` in the mode). 247 fs.errors.ResourceNotFound: If the path does not exist. 248 249 """ 250 251 @abc.abstractmethod 252 def remove(self, path): 253 # type: (Text) -> None 254 """Remove a file from the filesystem. 255 256 Arguments: 257 path (str): Path of the file to remove. 258 259 Raises: 260 fs.errors.FileExpected: If the path is a directory. 261 fs.errors.ResourceNotFound: If the path does not exist. 262 263 """ 264 265 @abc.abstractmethod 266 def removedir(self, path): 267 # type: (Text) -> None 268 """Remove a directory from the filesystem. 269 270 Arguments: 271 path (str): Path of the directory to remove. 272 273 Raises: 274 fs.errors.DirectoryNotEmpty: If the directory is not empty ( 275 see `~fs.base.FS.removetree` for a way to remove the 276 directory contents.). 277 fs.errors.DirectoryExpected: If the path does not refer to 278 a directory. 279 fs.errors.ResourceNotFound: If no resource exists at the 280 given path. 281 fs.errors.RemoveRootError: If an attempt is made to remove 282 the root directory (i.e. ``'/'``) 283 284 """ 285 286 @abc.abstractmethod 287 def setinfo(self, path, info): 288 # type: (Text, RawInfo) -> None 289 """Set info on a resource. 290 291 This method is the complement to `~fs.base.FS.getinfo` 292 and is used to set info values on a resource. 293 294 Arguments: 295 path (str): Path to a resource on the filesystem. 296 info (dict): Dictionary of resource info. 297 298 Raises: 299 fs.errors.ResourceNotFound: If ``path`` does not exist 300 on the filesystem 301 302 The ``info`` dict should be in the same format as the raw 303 info returned by ``getinfo(file).raw``. 304 305 Example: 306 >>> details_info = {"details": { 307 ... "modified": time.time() 308 ... }} 309 >>> my_fs.setinfo('file.txt', details_info) 310 311 """ 312 313 # ---------------------------------------------------------------- # 314 # Optional methods # 315 # Filesystems *may* implement these methods. # 316 # ---------------------------------------------------------------- # 317 318 def appendbytes(self, path, data): 319 # type: (Text, bytes) -> None 320 # FIXME(@althonos): accept bytearray and memoryview as well ? 321 """Append bytes to the end of a file, creating it if needed. 322 323 Arguments: 324 path (str): Path to a file. 325 data (bytes): Bytes to append. 326 327 Raises: 328 TypeError: If ``data`` is not a `bytes` instance. 329 fs.errors.ResourceNotFound: If a parent directory of 330 ``path`` does not exist. 331 332 """ 333 if not isinstance(data, bytes): 334 raise TypeError("must be bytes") 335 with self._lock: 336 with self.open(path, "ab") as append_file: 337 append_file.write(data) 338 339 def appendtext( 340 self, 341 path, # type: Text 342 text, # type: Text 343 encoding="utf-8", # type: Text 344 errors=None, # type: Optional[Text] 345 newline="", # type: Text 346 ): 347 # type: (...) -> None 348 """Append text to the end of a file, creating it if needed. 349 350 Arguments: 351 path (str): Path to a file. 352 text (str): Text to append. 353 encoding (str): Encoding for text files (defaults to ``utf-8``). 354 errors (str, optional): What to do with unicode decode errors 355 (see `codecs` module for more information). 356 newline (str): Newline parameter. 357 358 Raises: 359 TypeError: if ``text`` is not an unicode string. 360 fs.errors.ResourceNotFound: if a parent directory of 361 ``path`` does not exist. 362 363 """ 364 if not isinstance(text, six.text_type): 365 raise TypeError("must be unicode string") 366 with self._lock: 367 with self.open( 368 path, "at", encoding=encoding, errors=errors, newline=newline 369 ) as append_file: 370 append_file.write(text) 371 372 def close(self): 373 # type: () -> None 374 """Close the filesystem and release any resources. 375 376 It is important to call this method when you have finished 377 working with the filesystem. Some filesystems may not finalize 378 changes until they are closed (archives for example). You may 379 call this method explicitly (it is safe to call close multiple 380 times), or you can use the filesystem as a context manager to 381 automatically close. 382 383 Example: 384 >>> with OSFS('~/Desktop') as desktop_fs: 385 ... desktop_fs.writetext( 386 ... 'note.txt', 387 ... "Don't forget to tape Game of Thrones" 388 ... ) 389 390 If you attempt to use a filesystem that has been closed, a 391 `~fs.errors.FilesystemClosed` exception will be thrown. 392 393 """ 394 self._closed = True 395 396 def copy(self, src_path, dst_path, overwrite=False): 397 # type: (Text, Text, bool) -> None 398 """Copy file contents from ``src_path`` to ``dst_path``. 399 400 Arguments: 401 src_path (str): Path of source file. 402 dst_path (str): Path to destination file. 403 overwrite (bool): If `True`, overwrite the destination file 404 if it exists (defaults to `False`). 405 406 Raises: 407 fs.errors.DestinationExists: If ``dst_path`` exists, 408 and ``overwrite`` is `False`. 409 fs.errors.ResourceNotFound: If a parent directory of 410 ``dst_path`` does not exist. 411 412 """ 413 with self._lock: 414 if not overwrite and self.exists(dst_path): 415 raise errors.DestinationExists(dst_path) 416 with closing(self.open(src_path, "rb")) as read_file: 417 # FIXME(@althonos): typing complains because open return IO 418 self.upload(dst_path, read_file) # type: ignore 419 420 def copydir(self, src_path, dst_path, create=False): 421 # type: (Text, Text, bool) -> None 422 """Copy the contents of ``src_path`` to ``dst_path``. 423 424 Arguments: 425 src_path (str): Path of source directory. 426 dst_path (str): Path to destination directory. 427 create (bool): If `True`, then ``dst_path`` will be created 428 if it doesn't exist already (defaults to `False`). 429 430 Raises: 431 fs.errors.ResourceNotFound: If the ``dst_path`` 432 does not exist, and ``create`` is not `True`. 433 434 """ 435 with self._lock: 436 if not create and not self.exists(dst_path): 437 raise errors.ResourceNotFound(dst_path) 438 if not self.getinfo(src_path).is_dir: 439 raise errors.DirectoryExpected(src_path) 440 copy.copy_dir(self, src_path, self, dst_path) 441 442 def create(self, path, wipe=False): 443 # type: (Text, bool) -> bool 444 """Create an empty file. 445 446 The default behavior is to create a new file if one doesn't 447 already exist. If ``wipe`` is `True`, any existing file will 448 be truncated. 449 450 Arguments: 451 path (str): Path to a new file in the filesystem. 452 wipe (bool): If `True`, truncate any existing 453 file to 0 bytes (defaults to `False`). 454 455 Returns: 456 bool: `True` if a new file had to be created. 457 458 """ 459 with self._lock: 460 if not wipe and self.exists(path): 461 return False 462 with closing(self.open(path, "wb")): 463 pass 464 return True 465 466 def desc(self, path): 467 # type: (Text) -> Text 468 """Return a short descriptive text regarding a path. 469 470 Arguments: 471 path (str): A path to a resource on the filesystem. 472 473 Returns: 474 str: a short description of the path. 475 476 """ 477 if not self.exists(path): 478 raise errors.ResourceNotFound(path) 479 try: 480 syspath = self.getsyspath(path) 481 except (errors.ResourceNotFound, errors.NoSysPath): 482 return "{} on {}".format(path, self) 483 else: 484 return syspath 485 486 def exists(self, path): 487 # type: (Text) -> bool 488 """Check if a path maps to a resource. 489 490 Arguments: 491 path (str): Path to a resource. 492 493 Returns: 494 bool: `True` if a resource exists at the given path. 495 496 """ 497 try: 498 self.getinfo(path) 499 except errors.ResourceNotFound: 500 return False 501 else: 502 return True 503 504 def filterdir( 505 self, 506 path, # type: Text 507 files=None, # type: Optional[Iterable[Text]] 508 dirs=None, # type: Optional[Iterable[Text]] 509 exclude_dirs=None, # type: Optional[Iterable[Text]] 510 exclude_files=None, # type: Optional[Iterable[Text]] 511 namespaces=None, # type: Optional[Collection[Text]] 512 page=None, # type: Optional[Tuple[int, int]] 513 ): 514 # type: (...) -> Iterator[Info] 515 """Get an iterator of resource info, filtered by patterns. 516 517 This method enhances `~fs.base.FS.scandir` with additional 518 filtering functionality. 519 520 Arguments: 521 path (str): A path to a directory on the filesystem. 522 files (list, optional): A list of UNIX shell-style patterns 523 to filter file names, e.g. ``['*.py']``. 524 dirs (list, optional): A list of UNIX shell-style patterns 525 to filter directory names. 526 exclude_dirs (list, optional): A list of patterns used 527 to exclude directories. 528 exclude_files (list, optional): A list of patterns used 529 to exclude files. 530 namespaces (list, optional): A list of namespaces to include 531 in the resource information, e.g. ``['basic', 'access']``. 532 page (tuple, optional): May be a tuple of ``(<start>, <end>)`` 533 indexes to return an iterator of a subset of the resource 534 info, or `None` to iterate over the entire directory. 535 Paging a directory scan may be necessary for very large 536 directories. 537 538 Returns: 539 ~collections.abc.Iterator: an iterator of `Info` objects. 540 541 """ 542 resources = self.scandir(path, namespaces=namespaces) 543 filters = [] 544 545 def match_dir(patterns, info): 546 # type: (Optional[Iterable[Text]], Info) -> bool 547 """Pattern match info.name. 548 """ 549 return info.is_file or self.match(patterns, info.name) 550 551 def match_file(patterns, info): 552 # type: (Optional[Iterable[Text]], Info) -> bool 553 """Pattern match info.name. 554 """ 555 return info.is_dir or self.match(patterns, info.name) 556 557 def exclude_dir(patterns, info): 558 # type: (Optional[Iterable[Text]], Info) -> bool 559 """Pattern match info.name. 560 """ 561 return info.is_file or not self.match(patterns, info.name) 562 563 def exclude_file(patterns, info): 564 # type: (Optional[Iterable[Text]], Info) -> bool 565 """Pattern match info.name. 566 """ 567 return info.is_dir or not self.match(patterns, info.name) 568 569 if files: 570 filters.append(partial(match_file, files)) 571 if dirs: 572 filters.append(partial(match_dir, dirs)) 573 if exclude_dirs: 574 filters.append(partial(exclude_dir, exclude_dirs)) 575 if exclude_files: 576 filters.append(partial(exclude_file, exclude_files)) 577 578 if filters: 579 resources = ( 580 info for info in resources if all(_filter(info) for _filter in filters) 581 ) 582 583 iter_info = iter(resources) 584 if page is not None: 585 start, end = page 586 iter_info = itertools.islice(iter_info, start, end) 587 return iter_info 588 589 def readbytes(self, path): 590 # type: (Text) -> bytes 591 """Get the contents of a file as bytes. 592 593 Arguments: 594 path (str): A path to a readable file on the filesystem. 595 596 Returns: 597 bytes: the file contents. 598 599 Raises: 600 fs.errors.ResourceNotFound: if ``path`` does not exist. 601 602 """ 603 with closing(self.open(path, mode="rb")) as read_file: 604 contents = read_file.read() 605 return contents 606 607 getbytes = _new_name(readbytes, "getbytes") 608 609 def download(self, path, file, chunk_size=None, **options): 610 # type: (Text, BinaryIO, Optional[int], **Any) -> None 611 """Copies a file from the filesystem to a file-like object. 612 613 This may be more efficient that opening and copying files 614 manually if the filesystem supplies an optimized method. 615 616 Arguments: 617 path (str): Path to a resource. 618 file (file-like): A file-like object open for writing in 619 binary mode. 620 chunk_size (int, optional): Number of bytes to read at a 621 time, if a simple copy is used, or `None` to use 622 sensible default. 623 **options: Implementation specific options required to open 624 the source file. 625 626 Note that the file object ``file`` will *not* be closed by this 627 method. Take care to close it after this method completes 628 (ideally with a context manager). 629 630 Example: 631 >>> with open('starwars.mov', 'wb') as write_file: 632 ... my_fs.download('/movies/starwars.mov', write_file) 633 634 """ 635 with self._lock: 636 with self.openbin(path, **options) as read_file: 637 tools.copy_file_data(read_file, file, chunk_size=chunk_size) 638 639 getfile = _new_name(download, "getfile") 640 641 def readtext( 642 self, 643 path, # type: Text 644 encoding=None, # type: Optional[Text] 645 errors=None, # type: Optional[Text] 646 newline="", # type: Text 647 ): 648 # type: (...) -> Text 649 """Get the contents of a file as a string. 650 651 Arguments: 652 path (str): A path to a readable file on the filesystem. 653 encoding (str, optional): Encoding to use when reading contents 654 in text mode (defaults to `None`, reading in binary mode). 655 errors (str, optional): Unicode errors parameter. 656 newline (str): Newlines parameter. 657 658 Returns: 659 str: file contents. 660 661 Raises: 662 fs.errors.ResourceNotFound: If ``path`` does not exist. 663 664 """ 665 with closing( 666 self.open( 667 path, mode="rt", encoding=encoding, errors=errors, newline=newline 668 ) 669 ) as read_file: 670 contents = read_file.read() 671 return contents 672 673 gettext = _new_name(readtext, "gettext") 674 675 def getmeta(self, namespace="standard"): 676 # type: (Text) -> Mapping[Text, object] 677 """Get meta information regarding a filesystem. 678 679 Arguments: 680 namespace (str): The meta namespace (defaults 681 to ``"standard"``). 682 683 Returns: 684 dict: the meta information. 685 686 Meta information is associated with a *namespace* which may be 687 specified with the ``namespace`` parameter. The default namespace, 688 ``"standard"``, contains common information regarding the 689 filesystem's capabilities. Some filesystems may provide other 690 namespaces which expose less common or implementation specific 691 information. If a requested namespace is not supported by 692 a filesystem, then an empty dictionary will be returned. 693 694 The ``"standard"`` namespace supports the following keys: 695 696 =================== ============================================ 697 key Description 698 ------------------- -------------------------------------------- 699 case_insensitive `True` if this filesystem is case 700 insensitive. 701 invalid_path_chars A string containing the characters that 702 may not be used on this filesystem. 703 max_path_length Maximum number of characters permitted in 704 a path, or `None` for no limit. 705 max_sys_path_length Maximum number of characters permitted in 706 a sys path, or `None` for no limit. 707 network `True` if this filesystem requires a 708 network. 709 read_only `True` if this filesystem is read only. 710 supports_rename `True` if this filesystem supports an 711 `os.rename` operation. 712 =================== ============================================ 713 714 Most builtin filesystems will provide all these keys, and third- 715 party filesystems should do so whenever possible, but a key may 716 not be present if there is no way to know the value. 717 718 Note: 719 Meta information is constant for the lifetime of the 720 filesystem, and may be cached. 721 722 """ 723 if namespace == "standard": 724 meta = self._meta.copy() 725 else: 726 meta = {} 727 return meta 728 729 def getsize(self, path): 730 # type: (Text) -> int 731 """Get the size (in bytes) of a resource. 732 733 Arguments: 734 path (str): A path to a resource. 735 736 Returns: 737 int: the *size* of the resource. 738 739 The *size* of a file is the total number of readable bytes, 740 which may not reflect the exact number of bytes of reserved 741 disk space (or other storage medium). 742 743 The size of a directory is the number of bytes of overhead 744 use to store the directory entry. 745 746 """ 747 size = self.getdetails(path).size 748 return size 749 750 def getsyspath(self, path): 751 # type: (Text) -> Text 752 """Get the *system path* of a resource. 753 754 Parameters: 755 path (str): A path on the filesystem. 756 757 Returns: 758 str: the *system path* of the resource, if any. 759 760 Raises: 761 fs.errors.NoSysPath: If there is no corresponding system path. 762 763 A system path is one recognized by the OS, that may be used 764 outside of PyFilesystem (in an application or a shell for 765 example). This method will get the corresponding system path 766 that would be referenced by ``path``. 767 768 Not all filesystems have associated system paths. Network and 769 memory based filesystems, for example, may not physically store 770 data anywhere the OS knows about. It is also possible for some 771 paths to have a system path, whereas others don't. 772 773 This method will always return a str on Py3.* and unicode 774 on Py2.7. See `~getospath` if you need to encode the path as 775 bytes. 776 777 If ``path`` doesn't have a system path, a `~fs.errors.NoSysPath` 778 exception will be thrown. 779 780 Note: 781 A filesystem may return a system path even if no 782 resource is referenced by that path -- as long as it can 783 be certain what that system path would be. 784 785 """ 786 raise errors.NoSysPath(path=path) 787 788 def getospath(self, path): 789 # type: (Text) -> bytes 790 """Get a *system path* to a resource, encoded in the operating 791 system's prefered encoding. 792 793 Parameters: 794 path (str): A path on the filesystem. 795 796 Returns: 797 str: the *system path* of the resource, if any. 798 799 Raises: 800 fs.errors.NoSysPath: If there is no corresponding system path. 801 802 This method takes the output of `~getsyspath` and encodes it to 803 the filesystem's prefered encoding. In Python3 this step is 804 not required, as the `os` module will do it automatically. In 805 Python2.7, the encoding step is required to support filenames 806 on the filesystem that don't encode correctly. 807 808 Note: 809 If you want your code to work in Python2.7 and Python3 then 810 use this method if you want to work will the OS filesystem 811 outside of the OSFS interface. 812 813 """ 814 syspath = self.getsyspath(path) 815 ospath = fsencode(syspath) 816 return ospath 817 818 def gettype(self, path): 819 # type: (Text) -> ResourceType 820 """Get the type of a resource. 821 822 Parameters: 823 path (str): A path on the filesystem. 824 825 Returns: 826 ~fs.enums.ResourceType: the type of the resource. 827 828 A type of a resource is an integer that identifies the what 829 the resource references. The standard type integers may be one 830 of the values in the `~fs.enums.ResourceType` enumerations. 831 832 The most common resource types, supported by virtually all 833 filesystems are ``directory`` (1) and ``file`` (2), but the 834 following types are also possible: 835 836 =================== ====== 837 ResourceType value 838 ------------------- ------ 839 unknown 0 840 directory 1 841 file 2 842 character 3 843 block_special_file 4 844 fifo 5 845 socket 6 846 symlink 7 847 =================== ====== 848 849 Standard resource types are positive integers, negative values 850 are reserved for implementation specific resource types. 851 852 """ 853 resource_type = self.getdetails(path).type 854 return resource_type 855 856 def geturl(self, path, purpose="download"): 857 # type: (Text, Text) -> Text 858 """Get the URL to a given resource. 859 860 Parameters: 861 path (str): A path on the filesystem 862 purpose (str): A short string that indicates which URL 863 to retrieve for the given path (if there is more than 864 one). The default is ``'download'``, which should return 865 a URL that serves the file. Other filesystems may support 866 other values for ``purpose``. 867 868 Returns: 869 str: a URL. 870 871 Raises: 872 fs.errors.NoURL: If the path does not map to a URL. 873 874 """ 875 raise errors.NoURL(path, purpose) 876 877 def hassyspath(self, path): 878 # type: (Text) -> bool 879 """Check if a path maps to a system path. 880 881 Parameters: 882 path (str): A path on the filesystem. 883 884 Returns: 885 bool: `True` if the resource at ``path`` has a *syspath*. 886 887 """ 888 has_sys_path = True 889 try: 890 self.getsyspath(path) 891 except errors.NoSysPath: 892 has_sys_path = False 893 return has_sys_path 894 895 def hasurl(self, path, purpose="download"): 896 # type: (Text, Text) -> bool 897 """Check if a path has a corresponding URL. 898 899 Parameters: 900 path (str): A path on the filesystem. 901 purpose (str): A purpose parameter, as given in 902 `~fs.base.FS.geturl`. 903 904 Returns: 905 bool: `True` if an URL for the given purpose exists. 906 907 """ 908 has_url = True 909 try: 910 self.geturl(path, purpose=purpose) 911 except errors.NoURL: 912 has_url = False 913 return has_url 914 915 def isclosed(self): 916 # type: () -> bool 917 """Check if the filesystem is closed. 918 """ 919 return getattr(self, "_closed", False) 920 921 def isdir(self, path): 922 # type: (Text) -> bool 923 """Check if a path maps to an existing directory. 924 925 Parameters: 926 path (str): A path on the filesystem. 927 928 Returns: 929 bool: `True` if ``path`` maps to a directory. 930 931 """ 932 try: 933 return self.getinfo(path).is_dir 934 except errors.ResourceNotFound: 935 return False 936 937 def isempty(self, path): 938 # type: (Text) -> bool 939 """Check if a directory is empty. 940 941 A directory is considered empty when it does not contain 942 any file or any directory. 943 944 Parameters: 945 path (str): A path to a directory on the filesystem. 946 947 Returns: 948 bool: `True` if the directory is empty. 949 950 Raises: 951 errors.DirectoryExpected: If ``path`` is not a directory. 952 errors.ResourceNotFound: If ``path`` does not exist. 953 954 """ 955 return next(iter(self.scandir(path)), None) is None 956 957 def isfile(self, path): 958 # type: (Text) -> bool 959 """Check if a path maps to an existing file. 960 961 Parameters: 962 path (str): A path on the filesystem. 963 964 Returns: 965 bool: `True` if ``path`` maps to a file. 966 967 """ 968 try: 969 return not self.getinfo(path).is_dir 970 except errors.ResourceNotFound: 971 return False 972 973 def islink(self, path): 974 # type: (Text) -> bool 975 """Check if a path maps to a symlink. 976 977 Parameters: 978 path (str): A path on the filesystem. 979 980 Returns: 981 bool: `True` if ``path`` maps to a symlink. 982 983 """ 984 self.getinfo(path) 985 return False 986 987 def lock(self): 988 # type: () -> RLock 989 """Get a context manager that *locks* the filesystem. 990 991 Locking a filesystem gives a thread exclusive access to it. 992 Other threads will block until the threads with the lock has 993 left the context manager. 994 995 Returns: 996 threading.RLock: a lock specific to the filesystem instance. 997 998 Example: 999 >>> with my_fs.lock(): # May block 1000 ... # code here has exclusive access to the filesystem 1001 1002 It is a good idea to put a lock around any operations that you 1003 would like to be *atomic*. For instance if you are copying 1004 files, and you don't want another thread to delete or modify 1005 anything while the copy is in progress. 1006 1007 Locking with this method is only required for code that calls 1008 multiple filesystem methods. Individual methods are thread safe 1009 already, and don't need to be locked. 1010 1011 Note: 1012 This only locks at the Python level. There is nothing to 1013 prevent other processes from modifying the filesystem 1014 outside of the filesystem instance. 1015 1016 """ 1017 return self._lock 1018 1019 def movedir(self, src_path, dst_path, create=False): 1020 # type: (Text, Text, bool) -> None 1021 """Move directory ``src_path`` to ``dst_path``. 1022 1023 Parameters: 1024 src_path (str): Path of source directory on the filesystem. 1025 dst_path (str): Path to destination directory. 1026 create (bool): If `True`, then ``dst_path`` will be created 1027 if it doesn't exist already (defaults to `False`). 1028 1029 Raises: 1030 fs.errors.ResourceNotFound: if ``dst_path`` does not exist, 1031 and ``create`` is `False`. 1032 1033 """ 1034 with self._lock: 1035 if not create and not self.exists(dst_path): 1036 raise errors.ResourceNotFound(dst_path) 1037 move.move_dir(self, src_path, self, dst_path) 1038 1039 def makedirs( 1040 self, 1041 path, # type: Text 1042 permissions=None, # type: Optional[Permissions] 1043 recreate=False, # type: bool 1044 ): 1045 # type: (...) -> SubFS[FS] 1046 """Make a directory, and any missing intermediate directories. 1047 1048 Arguments: 1049 path (str): Path to directory from root. 1050 permissions (~fs.permissions.Permissions, optional): Initial 1051 permissions, or `None` to use defaults. 1052 recreate (bool): If `False` (the default), attempting to 1053 create an existing directory will raise an error. Set 1054 to `True` to ignore existing directories. 1055 1056 Returns: 1057 ~fs.subfs.SubFS: A sub-directory filesystem. 1058 1059 Raises: 1060 fs.errors.DirectoryExists: if the path is already 1061 a directory, and ``recreate`` is `False`. 1062 fs.errors.DirectoryExpected: if one of the ancestors 1063 in the path is not a directory. 1064 1065 """ 1066 self.check() 1067 with self._lock: 1068 dir_paths = tools.get_intermediate_dirs(self, path) 1069 for dir_path in dir_paths: 1070 try: 1071 self.makedir(dir_path, permissions=permissions) 1072 except errors.DirectoryExists: 1073 if not recreate: 1074 raise 1075 try: 1076 self.makedir(path, permissions=permissions) 1077 except errors.DirectoryExists: 1078 if not recreate: 1079 raise 1080 return self.opendir(path) 1081 1082 def move(self, src_path, dst_path, overwrite=False): 1083 # type: (Text, Text, bool) -> None 1084 """Move a file from ``src_path`` to ``dst_path``. 1085 1086 Arguments: 1087 src_path (str): A path on the filesystem to move. 1088 dst_path (str): A path on the filesystem where the source 1089 file will be written to. 1090 overwrite (bool): If `True`, destination path will be 1091 overwritten if it exists. 1092 1093 Raises: 1094 fs.errors.FileExpected: If ``src_path`` maps to a 1095 directory instead of a file. 1096 fs.errors.DestinationExists: If ``dst_path`` exists, 1097 and ``overwrite`` is `False`. 1098 fs.errors.ResourceNotFound: If a parent directory of 1099 ``dst_path`` does not exist. 1100 1101 """ 1102 if not overwrite and self.exists(dst_path): 1103 raise errors.DestinationExists(dst_path) 1104 if self.getinfo(src_path).is_dir: 1105 raise errors.FileExpected(src_path) 1106 if self.getmeta().get("supports_rename", False): 1107 try: 1108 src_sys_path = self.getsyspath(src_path) 1109 dst_sys_path = self.getsyspath(dst_path) 1110 except errors.NoSysPath: # pragma: no cover 1111 pass 1112 else: 1113 try: 1114 os.rename(src_sys_path, dst_sys_path) 1115 except OSError: 1116 pass 1117 else: 1118 return 1119 with self._lock: 1120 with self.open(src_path, "rb") as read_file: 1121 # FIXME(@althonos): typing complains because open return IO 1122 self.upload(dst_path, read_file) # type: ignore 1123 self.remove(src_path) 1124 1125 def open( 1126 self, 1127 path, # type: Text 1128 mode="r", # type: Text 1129 buffering=-1, # type: int 1130 encoding=None, # type: Optional[Text] 1131 errors=None, # type: Optional[Text] 1132 newline="", # type: Text 1133 **options # type: Any 1134 ): 1135 # type: (...) -> IO 1136 """Open a file. 1137 1138 Arguments: 1139 path (str): A path to a file on the filesystem. 1140 mode (str): Mode to open the file object with 1141 (defaults to *r*). 1142 buffering (int): Buffering policy (-1 to use 1143 default buffering, 0 to disable buffering, 1 to select 1144 line buffering, of any positive integer to indicate 1145 a buffer size). 1146 encoding (str): Encoding for text files (defaults to 1147 ``utf-8``) 1148 errors (str, optional): What to do with unicode decode errors 1149 (see `codecs` module for more information). 1150 newline (str): Newline parameter. 1151 **options: keyword arguments for any additional information 1152 required by the filesystem (if any). 1153 1154 Returns: 1155 io.IOBase: a *file-like* object. 1156 1157 Raises: 1158 fs.errors.FileExpected: If the path is not a file. 1159 fs.errors.FileExists: If the file exists, and *exclusive mode* 1160 is specified (``x`` in the mode). 1161 fs.errors.ResourceNotFound: If the path does not exist. 1162 1163 """ 1164 validate_open_mode(mode) 1165 bin_mode = mode.replace("t", "") 1166 bin_file = self.openbin(path, mode=bin_mode, buffering=buffering) 1167 io_stream = iotools.make_stream( 1168 path, 1169 bin_file, 1170 mode=mode, 1171 buffering=buffering, 1172 encoding=encoding or "utf-8", 1173 errors=errors, 1174 newline=newline, 1175 **options 1176 ) 1177 return io_stream 1178 1179 def opendir( 1180 self, # type: _F 1181 path, # type: Text 1182 factory=None, # type: Optional[_OpendirFactory] 1183 ): 1184 # type: (...) -> SubFS[FS] 1185 # FIXME(@althonos): use generics here if possible 1186 """Get a filesystem object for a sub-directory. 1187 1188 Arguments: 1189 path (str): Path to a directory on the filesystem. 1190 factory (callable, optional): A callable that when invoked 1191 with an FS instance and ``path`` will return a new FS object 1192 representing the sub-directory contents. If no ``factory`` 1193 is supplied then `~fs.subfs_class` will be used. 1194 1195 Returns: 1196 ~fs.subfs.SubFS: A filesystem representing a sub-directory. 1197 1198 Raises: 1199 fs.errors.DirectoryExpected: If ``dst_path`` does not 1200 exist or is not a directory. 1201 1202 """ 1203 from .subfs import SubFS 1204 1205 _factory = factory or self.subfs_class or SubFS 1206 1207 if not self.getbasic(path).is_dir: 1208 raise errors.DirectoryExpected(path=path) 1209 return _factory(self, path) 1210 1211 def removetree(self, dir_path): 1212 # type: (Text) -> None 1213 """Recursively remove the contents of a directory. 1214 1215 This method is similar to `~fs.base.removedir`, but will 1216 remove the contents of the directory if it is not empty. 1217 1218 Arguments: 1219 dir_path (str): Path to a directory on the filesystem. 1220 1221 """ 1222 _dir_path = abspath(normpath(dir_path)) 1223 with self._lock: 1224 walker = walk.Walker(search="depth") 1225 gen_info = walker.info(self, _dir_path) 1226 for _path, info in gen_info: 1227 if info.is_dir: 1228 self.removedir(_path) 1229 else: 1230 self.remove(_path) 1231 if _dir_path != "/": 1232 self.removedir(dir_path) 1233 1234 def scandir( 1235 self, 1236 path, # type: Text 1237 namespaces=None, # type: Optional[Collection[Text]] 1238 page=None, # type: Optional[Tuple[int, int]] 1239 ): 1240 # type: (...) -> Iterator[Info] 1241 """Get an iterator of resource info. 1242 1243 Arguments: 1244 path (str): A path to a directory on the filesystem. 1245 namespaces (list, optional): A list of namespaces to include 1246 in the resource information, e.g. ``['basic', 'access']``. 1247 page (tuple, optional): May be a tuple of ``(<start>, <end>)`` 1248 indexes to return an iterator of a subset of the resource 1249 info, or `None` to iterate over the entire directory. 1250 Paging a directory scan may be necessary for very large 1251 directories. 1252 1253 Returns: 1254 ~collections.abc.Iterator: an iterator of `Info` objects. 1255 1256 Raises: 1257 fs.errors.DirectoryExpected: If ``path`` is not a directory. 1258 fs.errors.ResourceNotFound: If ``path`` does not exist. 1259 1260 """ 1261 namespaces = namespaces or () 1262 _path = abspath(normpath(path)) 1263 1264 info = ( 1265 self.getinfo(join(_path, name), namespaces=namespaces) 1266 for name in self.listdir(path) 1267 ) 1268 iter_info = iter(info) 1269 if page is not None: 1270 start, end = page 1271 iter_info = itertools.islice(iter_info, start, end) 1272 return iter_info 1273 1274 def writebytes(self, path, contents): 1275 # type: (Text, bytes) -> None 1276 # FIXME(@althonos): accept bytearray and memoryview as well ? 1277 """Copy binary data to a file. 1278 1279 Arguments: 1280 path (str): Destination path on the filesystem. 1281 contents (bytes): Data to be written. 1282 1283 Raises: 1284 TypeError: if contents is not bytes. 1285 1286 """ 1287 if not isinstance(contents, bytes): 1288 raise TypeError("contents must be bytes") 1289 with closing(self.open(path, mode="wb")) as write_file: 1290 write_file.write(contents) 1291 1292 setbytes = _new_name(writebytes, "setbytes") 1293 1294 def upload(self, path, file, chunk_size=None, **options): 1295 # type: (Text, BinaryIO, Optional[int], **Any) -> None 1296 """Set a file to the contents of a binary file object. 1297 1298 This method copies bytes from an open binary file to a file on 1299 the filesystem. If the destination exists, it will first be 1300 truncated. 1301 1302 Arguments: 1303 path (str): A path on the filesystem. 1304 file (io.IOBase): a file object open for reading in 1305 binary mode. 1306 chunk_size (int, optional): Number of bytes to read at a 1307 time, if a simple copy is used, or `None` to use 1308 sensible default. 1309 **options: Implementation specific options required to open 1310 the source file. 1311 1312 Note that the file object ``file`` will *not* be closed by this 1313 method. Take care to close it after this method completes 1314 (ideally with a context manager). 1315 1316 Example: 1317 >>> with open('~/movies/starwars.mov', 'rb') as read_file: 1318 ... my_fs.upload('starwars.mov', read_file) 1319 1320 """ 1321 with self._lock: 1322 with self.openbin(path, mode="wb", **options) as dst_file: 1323 tools.copy_file_data(file, dst_file, chunk_size=chunk_size) 1324 1325 setbinfile = _new_name(upload, "setbinfile") 1326 1327 def writefile( 1328 self, 1329 path, # type: Text 1330 file, # type: IO 1331 encoding=None, # type: Optional[Text] 1332 errors=None, # type: Optional[Text] 1333 newline="", # type: Text 1334 ): 1335 # type: (...) -> None 1336 """Set a file to the contents of a file object. 1337 1338 Arguments: 1339 path (str): A path on the filesystem. 1340 file (io.IOBase): A file object open for reading. 1341 encoding (str, optional): Encoding of destination file, 1342 defaults to `None` for binary. 1343 errors (str, optional): How encoding errors should be treated 1344 (same as `io.open`). 1345 newline (str): Newline parameter (same as `io.open`). 1346 1347 This method is similar to `~FS.upload`, in that it copies data from a 1348 file-like object to a resource on the filesystem, but unlike ``upload``, 1349 this method also supports creating files in text-mode (if the ``encoding`` 1350 argument is supplied). 1351 1352 Note that the file object ``file`` will *not* be closed by this 1353 method. Take care to close it after this method completes 1354 (ideally with a context manager). 1355 1356 Example: 1357 >>> with open('myfile.txt') as read_file: 1358 ... my_fs.writefile('myfile.txt', read_file) 1359 1360 """ 1361 mode = "wb" if encoding is None else "wt" 1362 1363 with self._lock: 1364 with self.open( 1365 path, mode=mode, encoding=encoding, errors=errors, newline=newline 1366 ) as dst_file: 1367 tools.copy_file_data(file, dst_file) 1368 1369 setfile = _new_name(writefile, "setfile") 1370 1371 def settimes(self, path, accessed=None, modified=None): 1372 # type: (Text, Optional[datetime], Optional[datetime]) -> None 1373 """Set the accessed and modified time on a resource. 1374 1375 Arguments: 1376 path: A path to a resource on the filesystem. 1377 accessed (datetime, optional): The accessed time, or 1378 `None` (the default) to use the current time. 1379 modified (datetime, optional): The modified time, or 1380 `None` (the default) to use the same time as the 1381 ``accessed`` parameter. 1382 1383 """ 1384 details = {} # type: dict 1385 raw_info = {"details": details} 1386 1387 details["accessed"] = ( 1388 time.time() if accessed is None else datetime_to_epoch(accessed) 1389 ) 1390 1391 details["modified"] = ( 1392 details["accessed"] if modified is None else datetime_to_epoch(modified) 1393 ) 1394 1395 self.setinfo(path, raw_info) 1396 1397 def writetext( 1398 self, 1399 path, # type: Text 1400 contents, # type: Text 1401 encoding="utf-8", # type: Text 1402 errors=None, # type: Optional[Text] 1403 newline="", # type: Text 1404 ): 1405 # type: (...) -> None 1406 """Create or replace a file with text. 1407 1408 Arguments: 1409 path (str): Destination path on the filesystem. 1410 contents (str): Text to be written. 1411 encoding (str, optional): Encoding of destination file 1412 (defaults to ``'ut-8'``). 1413 errors (str, optional): How encoding errors should be treated 1414 (same as `io.open`). 1415 newline (str): Newline parameter (same as `io.open`). 1416 1417 Raises: 1418 TypeError: if ``contents`` is not a unicode string. 1419 1420 """ 1421 if not isinstance(contents, six.text_type): 1422 raise TypeError("contents must be unicode") 1423 with closing( 1424 self.open( 1425 path, mode="wt", encoding=encoding, errors=errors, newline=newline 1426 ) 1427 ) as write_file: 1428 write_file.write(contents) 1429 1430 settext = _new_name(writetext, "settext") 1431 1432 def touch(self, path): 1433 # type: (Text) -> None 1434 """Touch a file on the filesystem. 1435 1436 Touching a file means creating a new file if ``path`` doesn't 1437 exist, or update accessed and modified times if the path does 1438 exist. This method is similar to the linux command of the same 1439 name. 1440 1441 Arguments: 1442 path (str): A path to a file on the filesystem. 1443 1444 """ 1445 with self._lock: 1446 now = time.time() 1447 if not self.create(path): 1448 raw_info = {"details": {"accessed": now, "modified": now}} 1449 self.setinfo(path, raw_info) 1450 1451 def validatepath(self, path): 1452 # type: (Text) -> Text 1453 """Check if a path is valid, returning a normalized absolute 1454 path. 1455 1456 Many filesystems have restrictions on the format of paths they 1457 support. This method will check that ``path`` is valid on the 1458 underlaying storage mechanism and throw a 1459 `~fs.errors.InvalidPath` exception if it is not. 1460 1461 Arguments: 1462 path (str): A path. 1463 1464 Returns: 1465 str: A normalized, absolute path. 1466 1467 Raises: 1468 fs.errors.InvalidCharsInPath: If the path contains 1469 invalid characters. 1470 fs.errors.InvalidPath: If the path is invalid. 1471 fs.errors.FilesystemClosed: if the filesystem 1472 is closed. 1473 1474 """ 1475 self.check() 1476 1477 if isinstance(path, bytes): 1478 raise TypeError( 1479 "paths must be unicode (not str)" 1480 if six.PY2 1481 else "paths must be str (not bytes)" 1482 ) 1483 1484 meta = self.getmeta() 1485 1486 invalid_chars = typing.cast(six.text_type, meta.get("invalid_path_chars")) 1487 if invalid_chars: 1488 if set(path).intersection(invalid_chars): 1489 raise errors.InvalidCharsInPath(path) 1490 1491 max_sys_path_length = typing.cast(int, meta.get("max_sys_path_length", -1)) 1492 if max_sys_path_length != -1: 1493 try: 1494 sys_path = self.getsyspath(path) 1495 except errors.NoSysPath: # pragma: no cover 1496 pass 1497 else: 1498 if len(sys_path) > max_sys_path_length: 1499 _msg = "path too long (max {max_chars} characters in sys path)" 1500 msg = _msg.format(max_chars=max_sys_path_length) 1501 raise errors.InvalidPath(path, msg=msg) 1502 path = abspath(normpath(path)) 1503 return path 1504 1505 # ---------------------------------------------------------------- # 1506 # Helper methods # 1507 # Filesystems should not implement these methods. # 1508 # ---------------------------------------------------------------- # 1509 1510 def getbasic(self, path): 1511 # type: (Text) -> Info 1512 """Get the *basic* resource info. 1513 1514 This method is shorthand for the following:: 1515 1516 fs.getinfo(path, namespaces=['basic']) 1517 1518 Arguments: 1519 path (str): A path on the filesystem. 1520 1521 Returns: 1522 ~fs.info.Info: Resource information object for ``path``. 1523 1524 """ 1525 return self.getinfo(path, namespaces=["basic"]) 1526 1527 def getdetails(self, path): 1528 # type: (Text) -> Info 1529 """Get the *details* resource info. 1530 1531 This method is shorthand for the following:: 1532 1533 fs.getinfo(path, namespaces=['details']) 1534 1535 Arguments: 1536 path (str): A path on the filesystem. 1537 1538 Returns: 1539 ~fs.info.Info: Resource information object for ``path``. 1540 1541 """ 1542 return self.getinfo(path, namespaces=["details"]) 1543 1544 def check(self): 1545 # type: () -> None 1546 """Check if a filesystem may be used. 1547 1548 Raises: 1549 fs.errors.FilesystemClosed: if the filesystem is closed. 1550 1551 """ 1552 if self.isclosed(): 1553 raise errors.FilesystemClosed() 1554 1555 def match(self, patterns, name): 1556 # type: (Optional[Iterable[Text]], Text) -> bool 1557 """Check if a name matches any of a list of wildcards. 1558 1559 Arguments: 1560 patterns (list): A list of patterns, e.g. ``['*.py']`` 1561 name (str): A file or directory name (not a path) 1562 1563 Returns: 1564 bool: `True` if ``name`` matches any of the patterns. 1565 1566 If a filesystem is case *insensitive* (such as Windows) then 1567 this method will perform a case insensitive match (i.e. ``*.py`` 1568 will match the same names as ``*.PY``). Otherwise the match will 1569 be case sensitive (``*.py`` and ``*.PY`` will match different 1570 names). 1571 1572 Example: 1573 >>> home_fs.match(['*.py'], '__init__.py') 1574 True 1575 >>> home_fs.match(['*.jpg', '*.png'], 'foo.gif') 1576 False 1577 1578 Note: 1579 If ``patterns`` is `None` (or ``['*']``), then this 1580 method will always return `True`. 1581 1582 """ 1583 if patterns is None: 1584 return True 1585 if isinstance(patterns, six.text_type): 1586 raise TypeError("patterns must be a list or sequence") 1587 case_sensitive = not typing.cast( 1588 bool, self.getmeta().get("case_insensitive", False) 1589 ) 1590 matcher = wildcard.get_matcher(patterns, case_sensitive) 1591 return matcher(name) 1592 1593 def tree(self, **kwargs): 1594 # type: (**Any) -> None 1595 """Render a tree view of the filesystem to stdout or a file. 1596 1597 The parameters are passed to :func:`~fs.tree.render`. 1598 1599 Keyword Arguments: 1600 path (str): The path of the directory to start rendering 1601 from (defaults to root folder, i.e. ``'/'``). 1602 file (io.IOBase): An open file-like object to render the 1603 tree, or `None` for stdout. 1604 encoding (str): Unicode encoding, or `None` to 1605 auto-detect. 1606 max_levels (int): Maximum number of levels to 1607 display, or `None` for no maximum. 1608 with_color (bool): Enable terminal color output, 1609 or `None` to auto-detect terminal. 1610 dirs_first (bool): Show directories first. 1611 exclude (list): Option list of directory patterns 1612 to exclude from the tree render. 1613 filter (list): Optional list of files patterns to 1614 match in the tree render. 1615 1616 """ 1617 from .tree import render 1618 1619 render(self, **kwargs) 1620 1621 def hash(self, path, name): 1622 # type: (Text, Text) -> Text 1623 """Get the hash of a file's contents. 1624 1625 Arguments: 1626 path(str): A path on the filesystem. 1627 name(str): 1628 One of the algorithms supported by the hashlib module, e.g. `"md5"` 1629 1630 Returns: 1631 str: The hex digest of the hash. 1632 1633 Raises: 1634 fs.errors.UnsupportedHash: If the requested hash is not supported. 1635 1636 """ 1637 self.validatepath(path) 1638 try: 1639 hash_object = hashlib.new(name) 1640 except ValueError: 1641 raise errors.UnsupportedHash("hash '{}' is not supported".format(name)) 1642 with self.openbin(path) as binary_file: 1643 while True: 1644 chunk = binary_file.read(1024 * 1024) 1645 if not chunk: 1646 break 1647 hash_object.update(chunk) 1648 return hash_object.hexdigest() 1649