source: ftputil/file.py @ 1459:defd9fc5cd71

Last change on this file since 1459:defd9fc5cd71 was 1459:defd9fc5cd71, checked in by Stefan Schwarzer <sschwarzer@…>, 6 years ago
Dealt with many of PyLint's messages. In some places I changed the code as implicitly advised. In other places I disabled the PyLint messages but commented why the code "violates" PyLint's rules.
File size: 10.4 KB
Line 
1# Copyright (C) 2003-2013, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# Copyright (C) 2008, Roger Demetrescu <roger.demetrescu@gmail.com>
3# See the file LICENSE for licensing terms.
4
5"""
6ftputil.file - support for file-like objects on FTP servers
7"""
8
9from __future__ import print_function
10from __future__ import unicode_literals
11
12import io
13
14import ftputil.compat
15import ftputil.error
16
17
18# This module shouldn't be used by clients of the ftputil library.
19__all__ = []
20
21
22class BufferedIO(io.BufferedIOBase):
23    """
24    Adapt a file object returned from `socket.makefile` to the
25    interfaces of `io.BufferedReader` or `io.BufferedWriter`, so that
26    the new object can be wrapped by `io.TextIOWrapper`.
27
28    This is only needed with Python 2, since in Python 3
29    `socket.makefile` already returns a `BufferedReader` or
30    `BufferedWriter` object (depending on mode).
31    """
32
33    def __init__(self, fobj, is_readable=False, is_writable=False):
34        # Don't call baseclass constructor for this adapter.
35        # pylint: disable=super-init-not-called
36        #
37        # This is the return value of `socket.makefile` and is already
38        # buffered.
39        self.raw = fobj
40        self._is_readable = is_readable
41        self._is_writable = is_writable
42
43    @property
44    def closed(self):
45        # pylint: disable=missing-docstring
46        return self.raw.closed
47
48    def close(self):
49        self.raw.close()
50
51    def fileno(self):
52        return self.raw.fileno()
53
54    def isatty(self):
55        # It's highly unlikely that this file is interactive.
56        return False
57
58    def seekable(self):
59        return False
60
61    #
62    # Interface for `BufferedReader`
63    #
64    def readable(self):
65        return self._is_readable
66
67    def read(self, *arg):
68        return self.raw.read(*arg)
69
70    read1 = read
71
72    def readline(self, *arg):
73        return self.raw.readline(*arg)
74
75    def readlines(self, *arg):
76        return self.raw.readlines(*arg)
77
78    def readinto(self, bytearray_):
79        data = self.raw.read(len(bytearray_))
80        bytearray_[:len(data)] = data
81        return len(data)
82
83    #
84    # Interface for `BufferedWriter`
85    #
86    def writable(self):
87        return self._is_writable
88
89    def flush(self):
90        self.raw.flush()
91
92    # Derived from `socket.py` in Python 2.6 and 2.7.
93    # There doesn't seem to be a public API for this.
94    def _write_buffer_size(self):
95        """Return current size of the write buffer in bytes."""
96        # pylint: disable=protected-access
97        if hasattr(self.raw, "_wbuf_len"):
98            # Python 2.6.3 - 2.7.5
99            return self.raw._wbuf_len
100        elif hasattr(self.raw, "_get_wbuf_len"):
101            # Python 2.6 - 2.6.2. (Strictly speaking, all other
102            # Python 2.6 versions have a `_get_wbuf_len` method, but
103            # for 2.6.3 and up it returns `_wbuf_len`).
104            return self.raw._get_wbuf_len()
105        else:
106            # Fallback. In the context of `write` this means the file
107            # appears to be unbuffered.
108            return 0
109
110    def write(self, bytes_or_bytearray):
111        # `BufferedWriter.write` has to return the number of written
112        # bytes, but files returned from `socket.makefile` in Python 2
113        # return `None`. Hence provide a workaround.
114        old_buffer_byte_count = self._write_buffer_size()
115        added_byte_count = len(bytes_or_bytearray)
116        self.raw.write(bytes_or_bytearray)
117        new_buffer_byte_count = self._write_buffer_size()
118        return (old_buffer_byte_count + added_byte_count -
119                new_buffer_byte_count)
120
121    def writelines(self, lines):
122        self.raw.writelines(lines)
123
124
125class FTPFile(object):
126    """
127    Represents a file-like object associated with an FTP host. File
128    and socket are closed appropriately if the `close` method is
129    called.
130    """
131
132    # Set timeout in seconds when closing file connections (see ticket #51).
133    _close_timeout = 5
134
135    def __init__(self, host):
136        """Construct the file(-like) object."""
137        self._host = host
138        # pylint: disable=protected-access
139        self._session = host._session
140        # The file is still closed.
141        self.closed = True
142        self._conn = None
143        self._fobj = None
144
145    def _open(self, path, mode, buffering=None, encoding=None, errors=None,
146              newline=None):
147        """
148        Open the remote file with given path name and mode.
149
150        Contrary to the `open` builtin, this method returns `None`,
151        instead this file object is modified in-place.
152        """
153        # We use the same arguments as in `io.open`.
154        # pylint: disable=too-many-arguments
155        #
156        # `buffering` argument isn't used at this time.
157        # pylint: disable=unused-argument
158        #
159        # Python 3's `socket.makefile` supports the same interface as
160        # the new `open` builtin, but Python 2 supports only a mode,
161        # but doesn't return an object with the proper interface to
162        # wrap it in `io.TextIOWrapper`.
163        #
164        # Therefore, to make the code work on Python 2 _and_ 3, use
165        # `socket.makefile` to always create a binary file and under
166        # Python 2 wrap it in an adapter class.
167        #
168        # Check mode.
169        if "a" in mode:
170            raise ftputil.error.FTPIOError("append mode not supported")
171        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
172            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
173        if "b" in mode and "t" in mode:
174            # Raise a `ValueError` like Python would.
175            raise ValueError("can't have text and binary mode at once")
176        # Convenience variables
177        is_binary_mode = "b" in mode
178        is_read_mode = "r" in mode
179        # Always use binary mode (see above).
180        transfer_type = "I"
181        command = "TYPE {0}".format(transfer_type)
182        with ftputil.error.ftplib_error_to_ftp_io_error:
183            self._session.voidcmd(command)
184        # Make transfer command.
185        command_type = ("STOR", "RETR")[is_read_mode]
186        command = "{0} {1}".format(command_type, path)
187        # Force to binary regardless of transfer type (see above).
188        makefile_mode = mode
189        makefile_mode = makefile_mode.replace("t", "")
190        if not "b" in makefile_mode:
191            makefile_mode += "b"
192        # Get connection and file object.
193        with ftputil.error.ftplib_error_to_ftp_io_error:
194            self._conn = self._session.transfercmd(command)
195        # The file object. Under Python 3, this will already be a
196        # `BufferedReader` or `BufferedWriter` object.
197        fobj = self._conn.makefile(makefile_mode)
198        if ftputil.compat.python_version == 2:
199            if is_read_mode:
200                fobj = BufferedIO(fobj, is_readable=True)
201            else:
202                fobj = BufferedIO(fobj, is_writable=True)
203        if not is_binary_mode:
204            fobj = io.TextIOWrapper(fobj, encoding=encoding,
205                                    errors=errors, newline=newline)
206        self._fobj = fobj
207        # This comes last so that `close` won't try to close `FTPFile`
208        # objects without `_conn` and `_fobj` attributes in case of an
209        # error.
210        self.closed = False
211
212    def __iter__(self):
213        """Return a file iterator."""
214        return self
215
216    def __next__(self):
217        """
218        Return the next line or raise `StopIteration`, if there are
219        no more.
220        """
221        # Apply implicit line ending conversion for text files.
222        line = self.readline()
223        if line:
224            return line
225        else:
226            raise StopIteration
227
228    # Although Python 2.6+ has the `next` builtin function already, it
229    # still requires iterators to have a `next` method.
230    next = __next__
231
232    #
233    # Context manager methods
234    #
235    def __enter__(self):
236        # Return `self`, so it can be accessed as the variable
237        # component of the `with` statement.
238        return self
239
240    def __exit__(self, exc_type, exc_val, exc_tb):
241        # We don't need the `exc_*` arguments here
242        # pylint: disable=unused-argument
243        self.close()
244        # Be explicit
245        return False
246
247    #
248    # Other attributes
249    #
250    def __getattr__(self, attr_name):
251        """
252        Handle requests for attributes unknown to `FTPFile` objects:
253        delegate the requests to the contained file object.
254        """
255        if attr_name in ("encoding flush isatty fileno read readline "
256                         "readlines seek tell truncate name softspace "
257                         "write writelines".split()):
258            return getattr(self._fobj, attr_name)
259        raise AttributeError(
260                "'FTPFile' object has no attribute '{0}'".format(attr_name))
261
262    # TODO: Implement `__dir__`? (See
263    # http://docs.python.org/py3k/whatsnew/2.6.html#other-language-changes )
264
265    def close(self):
266        """Close the `FTPFile`."""
267        if self.closed:
268            return
269        # Timeout value to restore, see below.
270        # Statement works only before the try/finally statement,
271        # otherwise Python raises an `UnboundLocalError`.
272        old_timeout = self._session.sock.gettimeout()
273        try:
274            self._fobj.close()
275            self._fobj = None
276            with ftputil.error.ftplib_error_to_ftp_io_error:
277                self._conn.close()
278            # Set a timeout to prevent waiting until server timeout
279            # if we have a server blocking here like in ticket #51.
280            self._session.sock.settimeout(self._close_timeout)
281            try:
282                with ftputil.error.ftplib_error_to_ftp_io_error:
283                    self._session.voidresp()
284            except ftputil.error.FTPIOError as exc:
285                # Ignore some errors, see tickets #51 and #17 at
286                # http://ftputil.sschwarzer.net/trac/ticket/51 and
287                # http://ftputil.sschwarzer.net/trac/ticket/17,
288                # respectively.
289                exc = str(exc)
290                error_code = exc[:3]
291                if exc.splitlines()[0] != "timed out" and \
292                  error_code not in ("150", "426", "450", "451"):
293                    raise
294        finally:
295            # Restore timeout for socket of `FTPFile`'s `ftplib.FTP`
296            # object in case the connection is reused later.
297            self._session.sock.settimeout(old_timeout)
298            # If something went wrong before, the file is probably
299            # defunct and subsequent calls to `close` won't help
300            # either, so we consider the file closed for practical
301            # purposes.
302            self.closed = True
Note: See TracBrowser for help on using the repository browser.