source: ftputil/path.py @ 1684:a0a8365cdb12

Last change on this file since 1684:a0a8365cdb12 was 1684:a0a8365cdb12, checked in by Stefan Schwarzer <sschwarzer@…>, 22 months ago
Extract method `_is_file_system_entity` Extract `_is_file_system_entity` as the common code of `ftputil.path._Path.isdir` and `ftputil.path._Path.isfile`. For this change, I'm a bit torn between increasing the complexity by another indirection vs. removing almost all redundancy between the two methods. For now, favor the redundancy removal.
File size: 8.1 KB
Line 
1# Copyright (C) 2003-2013, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
3# See the file LICENSE for licensing terms.
4
5"""
6ftputil.path - simulate `os.path` for FTP servers
7"""
8
9from __future__ import absolute_import
10from __future__ import unicode_literals
11
12import posixpath
13import stat
14
15import ftputil.compat
16import ftputil.error
17import ftputil.tool
18
19
20# The `_Path` class shouldn't be used directly by clients of the
21# ftputil library.
22__all__ = []
23
24
25class _Path(object):
26    """
27    Support class resembling `os.path`, accessible from the `FTPHost`
28    object, e. g. as `FTPHost().path.abspath(path)`.
29
30    Hint: substitute `os` with the `FTPHost` object.
31    """
32
33    # `_Path` needs to provide all methods of `os.path`.
34    # pylint: disable=too-many-instance-attributes
35
36    def __init__(self, host):
37        self._host = host
38        # Delegate these to the `posixpath` module.
39        # pylint: disable=invalid-name
40        pp = posixpath
41        self.dirname      = pp.dirname
42        self.basename     = pp.basename
43        self.isabs        = pp.isabs
44        self.commonprefix = pp.commonprefix
45        self.split        = pp.split
46        self.splitdrive   = pp.splitdrive
47        self.splitext     = pp.splitext
48        self.normcase     = pp.normcase
49        self.normpath     = pp.normpath
50
51    def abspath(self, path):
52        """Return an absolute path."""
53        original_path = path
54        path = ftputil.tool.as_unicode(path)
55        if not self.isabs(path):
56            path = self.join(self._host.getcwd(), path)
57        return ftputil.tool.same_string_type_as(original_path,
58                                                self.normpath(path))
59
60    def exists(self, path):
61        """Return true if the path exists."""
62        try:
63            lstat_result = self._host.lstat(
64                             path, _exception_for_missing_path=False)
65            return lstat_result is not None
66        except ftputil.error.RootDirError:
67            return True
68
69    def getmtime(self, path):
70        """
71        Return the timestamp for the last modification for `path`
72        as a float.
73
74        This will raise `PermanentError` if the path doesn't exist,
75        but maybe other exceptions depending on the state of the
76        server (e. g. timeout).
77        """
78        return self._host.stat(path).st_mtime
79
80    def getsize(self, path):
81        """
82        Return the size of the `path` item as an integer.
83
84        This will raise `PermanentError` if the path doesn't exist,
85        but maybe raise other exceptions depending on the state of the
86        server (e. g. timeout).
87        """
88        return self._host.stat(path).st_size
89
90    @staticmethod
91    def join(*paths):
92        """
93        Join the path component from `paths` and return the joined
94        path.
95
96        All of these paths must be either unicode strings or byte
97        strings. If not, `join` raises a `TypeError`.
98        """
99        # These checks are implicitly done by Python 3, but not by
100        # Python 2.
101        all_paths_are_unicode = all(
102          (isinstance(path, ftputil.compat.unicode_type)
103          for path in paths))
104        all_paths_are_bytes = all(
105          (isinstance(path, ftputil.compat.bytes_type)
106          for path in paths))
107        if all_paths_are_unicode or all_paths_are_bytes:
108            return posixpath.join(*paths)
109        else:
110            # Python 3 raises this exception for mixed strings
111            # in `os.path.join`, so also use this exception.
112            raise TypeError(
113                    "can't mix unicode strings and bytes in path components")
114
115    # Check whether a path is a regular file/dir/link. For the first
116    # two cases follow links (like in `os.path`).
117    #
118    # Implementation note: The previous implementations simply called
119    # `stat` or `lstat` and returned `False` if they ended with
120    # raising a `PermanentError`. That exception usually used to
121    # signal a missing path. This approach has the problem, however,
122    # that exceptions caused by code earlier in `lstat` are obscured
123    # by the exception handling in `isfile`, `isdir` and `islink`.
124
125    def _is_file_system_entity(self, path, dir_or_file):
126        """
127        Return `True` if `path` represents the file system entity
128        described by `dir_or_file` ("dir" or "file").
129
130        Return `False` if `path` isn't a directory or file,
131        respectively or if `path` leads to an infinite chain of links.
132        """
133        assert dir_or_file in ["dir", "file"]
134        # Consider differences between directories and files.
135        if dir_or_file == "dir":
136            should_look_for_dir = True
137            stat_function = stat.S_ISDIR
138        else:
139            should_look_for_dir = False
140            stat_function = stat.S_ISREG
141        #
142        path = ftputil.tool.as_unicode(path)
143        #  Workaround if we can't go up from the current directory.
144        #  The result from `getcwd` should already be normalized.
145        if self.normpath(path) == self._host.getcwd():
146            return should_look_for_dir
147        try:
148            stat_result = self._host.stat(
149                            path, _exception_for_missing_path=False)
150        except ftputil.error.RecursiveLinksError:
151            return False
152        except ftputil.error.RootDirError:
153            return should_look_for_dir
154        else:
155            if stat_result is None:
156                # Non-existent path
157                return False
158            else:
159                return stat_function(stat_result.st_mode)
160
161    def isdir(self, path):
162        """
163        Return true if the `path` exists and corresponds to a
164        directory (no link).
165
166        A non-existing path does _not_ cause a `PermanentError`.
167        """
168        return self._is_file_system_entity(path, "dir")
169
170    def isfile(self, path):
171        """
172        Return true if the `path` exists and corresponds to a regular
173        file (no link).
174
175        A non-existing path does _not_ cause a `PermanentError`.
176        """
177        return self._is_file_system_entity(path, "file")
178
179    def islink(self, path):
180        """
181        Return true if the `path` exists and is a link.
182
183        A non-existing path does _not_ cause a `PermanentError`.
184        """
185        path = ftputil.tool.as_unicode(path)
186        try:
187            lstat_result = self._host.lstat(
188                             path, _exception_for_missing_path=False)
189        except ftputil.error.RootDirError:
190            return False
191        else:
192            if lstat_result is None:
193                # Non-existent path
194                return False
195            else:
196                return stat.S_ISLNK(lstat_result.st_mode)
197
198    def walk(self, top, func, arg):
199        """
200        Directory tree walk with callback function.
201
202        For each directory in the directory tree rooted at top
203        (including top itself, but excluding "." and ".."), call
204        func(arg, dirname, fnames). dirname is the name of the
205        directory, and fnames a list of the names of the files and
206        subdirectories in dirname (excluding "." and "..").  func may
207        modify the fnames list in-place (e.g. via del or slice
208        assignment), and walk will only recurse into the
209        subdirectories whose names remain in fnames; this can be used
210        to implement a filter, or to impose a specific order of
211        visiting.  No semantics are defined for, or required of, arg,
212        beyond that arg is always passed to func.  It can be used,
213        e.g., to pass a filename pattern, or a mutable object designed
214        to accumulate statistics.  Passing None for arg is common.
215        """
216        top = ftputil.tool.as_unicode(top)
217        # This code (and the above documentation) is taken from
218        # `posixpath.py`, with slight modifications.
219        try:
220            names = self._host.listdir(top)
221        except OSError:
222            return
223        func(arg, top, names)
224        for name in names:
225            name = self.join(top, name)
226            try:
227                stat_result = self._host.lstat(name)
228            except OSError:
229                continue
230            if stat.S_ISDIR(stat_result[stat.ST_MODE]):
231                self.walk(name, func, arg)
Note: See TracBrowser for help on using the repository browser.