source: ftputil/path.py @ 1713:f146a1ea66aa

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