source: ftputil/file.py @ 1341:8d4ee79423cc

Last change on this file since 1341:8d4ee79423cc was 1341:8d4ee79423cc, checked in by Stefan Schwarzer <sschwarzer@…>, 7 years ago
Slightly more readable indentation (IMHO).
File size: 9.2 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 BufferedReaderWriter(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        # This is the return value of `socket.makefile` and is already
35        # buffered.
36        self.raw = fobj
37        self._is_readable = is_readable
38        self._is_writable = is_writable
39
40    @property
41    def closed(self):
42        return self.raw.closed
43
44    def close(self):
45        self.raw.close()
46
47    def fileno(self):
48        return self.raw.fileno()
49
50    def isatty(self):
51        # It's highly unlikely that this is interactive.
52        return False
53
54    def seekable(self):
55        return False
56
57    #
58    # Interface for `BufferedReader`
59    #
60    def readable(self):
61        return self._is_readable
62
63    def read(self, *arg):
64        return self.raw.read(*arg)
65
66    read1 = read
67
68    def readline(self, *arg):
69        return self.raw.readline(*arg)
70
71    def readlines(self, *arg):
72        return self.raw.readlines(*arg)
73
74    def readinto(self, bytearray_):
75        data = self.raw.read(len(bytearray_))
76        bytearray_[:len(data)] = data
77        return len(data)
78
79    #
80    # Interface for `BufferedWriter`
81    #
82    def writable(self):
83        return self._is_writable
84
85    def flush(self):
86        self.raw.flush()
87
88    def write(self, bytes_or_bytearray):
89        # `BufferedWriter.write` has to return the number of written
90        # bytes. Since we don't really know how many bytes got
91        # actually written, return the length of the full data, but
92        # also call `flush` to increase the chance that all bytes are
93        # written.
94        self.raw.write(bytes_or_bytearray)
95        # TODO: Measure impact of flushing for many small writes.
96        self.flush()
97        return len(bytes_or_bytearray)
98
99    def writelines(self, lines):
100        self.raw.writelines(lines)
101
102
103class _FTPFile(object):
104    """
105    Represents a file-like object associated with an FTP host. File
106    and socket are closed appropriately if the `close` method is
107    called.
108    """
109
110    # Set timeout in seconds when closing file connections (see ticket #51).
111    _close_timeout = 5
112
113    def __init__(self, host):
114        """Construct the file(-like) object."""
115        self._host = host
116        self._session = host._session
117        # The file is still closed.
118        self.closed = True
119        self._conn = None
120        self._fobj = None
121
122    def _open(self, path, mode, buffering=None, encoding=None, errors=None,
123              newline=None):
124        """
125        Open the remote file with given path name and mode.
126
127        Contrary to the `open` builtin, this method returns `None`,
128        instead this file object is modified in-place.
129        """
130        # Python 3's `socket.makefile` supports the same interface as
131        # the new `open` builtin, but Python 2 supports only a mode,
132        # but doesn't return an object with the proper interface to
133        # wrap it in `io.TextIOWrapper`.
134        #
135        # Therefore, to make the code work on Python 2 _and_ 3, use
136        # `socket.makefile` to always create a binary file and under
137        # Python 2 wrap it in an adapter class.
138        #
139        # Check mode.
140        if "a" in mode:
141            raise ftputil.error.FTPIOError("append mode not supported")
142        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
143            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
144        if "b" in mode and "t" in mode:
145            # Raise a `ValueError` like Python would.
146            raise ValueError("can't have text and binary mode at once")
147        # Convenience variables
148        is_bin_mode = "b" in mode
149        is_read_mode = "r" in mode
150        # Always use binary mode (see above).
151        transfer_type = "I"
152        command = 'TYPE {0}'.format(transfer_type)
153        with ftputil.error.ftplib_error_to_ftp_io_error:
154            self._session.voidcmd(command)
155        # Make transfer command.
156        command_type = ('STOR', 'RETR')[is_read_mode]
157        command = '{0} {1}'.format(command_type, path)
158        # Force to binary regardless of transfer type (see above).
159        makefile_mode = mode
160        makefile_mode = makefile_mode.replace("t", "")
161        if not "b" in makefile_mode:
162            makefile_mode += "b"
163        # Get connection and file object.
164        with ftputil.error.ftplib_error_to_ftp_io_error:
165            self._conn = self._session.transfercmd(command)
166        # The file object. Under Python 3, this will already be a
167        # `BufferedReader` or `BufferedWriter` object.
168        fobj = self._conn.makefile(makefile_mode)
169        if ftputil.compat.python_version == 2:
170            if is_read_mode:
171                fobj = BufferedReaderWriter(fobj, is_readable=True)
172            else:
173                fobj = BufferedReaderWriter(fobj, is_writable=True)
174        if not is_bin_mode:
175            fobj = io.TextIOWrapper(fobj, encoding=encoding,
176                                    errors=errors, newline=newline)
177        self._fobj = fobj
178        # This comes last so that `close` won't try to close `_FTPFile`
179        # objects without `_conn` and `_fobj` attributes in case of an
180        # error.
181        self.closed = False
182
183    def __iter__(self):
184        """Return a file iterator."""
185        return self
186
187    def __next__(self):
188        """
189        Return the next line or raise `StopIteration`, if there are
190        no more.
191        """
192        # Apply implicit line ending conversion for text files.
193        line = self.readline()
194        if line:
195            return line
196        else:
197            raise StopIteration
198
199    # Although Python 2.6+ has the `next` builtin function already, it
200    # still requires iterators to have a `next` method.
201    next = __next__
202
203    #
204    # Context manager methods
205    #
206    def __enter__(self):
207        # Return `self`, so it can be accessed as the variable
208        # component of the `with` statement.
209        return self
210
211    def __exit__(self, exc_type, exc_val, exc_tb):
212        # We don't need the `exc_*` arguments here
213        # pylint: disable=W0613
214        self.close()
215        # Be explicit
216        return False
217
218    #
219    # Other attributes
220    #
221    def __getattr__(self, attr_name):
222        """
223        Handle requests for attributes unknown to `_FTPFile` objects:
224        delegate the requests to the contained file object.
225        """
226        if attr_name in ("encoding flush isatty fileno read readline "
227                         "readlines seek tell truncate name softspace "
228                         "write writelines".split()):
229            return getattr(self._fobj, attr_name)
230        raise AttributeError(
231                "'FTPFile' object has no attribute '{0}'".format(attr_name))
232
233    # TODO: Implement `__dir__`? (See
234    # http://docs.python.org/py3k/whatsnew/2.6.html#other-language-changes )
235
236    def close(self):
237        """Close the `FTPFile`."""
238        if self.closed:
239            return
240        # Timeout value to restore, see below.
241        # Statement works only before the try/finally statement,
242        # otherwise Python raises an `UnboundLocalError`.
243        old_timeout = self._session.sock.gettimeout()
244        try:
245            self._fobj.close()
246            self._fobj = None
247            with ftputil.error.ftplib_error_to_ftp_io_error:
248                self._conn.close()
249            # Set a timeout to prevent waiting until server timeout
250            # if we have a server blocking here like in ticket #51.
251            self._session.sock.settimeout(self._close_timeout)
252            try:
253                with ftputil.error.ftplib_error_to_ftp_io_error:
254                    self._session.voidresp()
255            except ftputil.error.FTPIOError as exc:
256                # Ignore some errors, see tickets #51 and #17 at
257                # http://ftputil.sschwarzer.net/trac/ticket/51 and
258                # http://ftputil.sschwarzer.net/trac/ticket/17,
259                # respectively.
260                exc = str(exc)
261                error_code = exc[:3]
262                if exc.splitlines()[0] != "timed out" and \
263                  error_code not in ("150", "426", "450", "451"):
264                    raise
265        finally:
266            # Restore timeout for socket of `_FTPFile`'s `ftplib.FTP`
267            # object in case the connection is reused later.
268            self._session.sock.settimeout(old_timeout)
269            # If something went wrong before, the file is probably
270            # defunct and subsequent calls to `close` won't help
271            # either, so we consider the file closed for practical
272            # purposes.
273            self.closed = True
Note: See TracBrowser for help on using the repository browser.