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