source: ftputil/file.py @ 1343:bcadc5b1504f

Last change on this file since 1343:bcadc5b1504f was 1343:bcadc5b1504f, checked in by Stefan Schwarzer <sschwarzer@…>, 7 years ago
Use double quotes (") for strings. I used to have a rule to put "identifier-like" strings in single quotes (') and other strings in double quotes. For example, dictionary keys would usually be in single quotes, but error messages in double quotes. Unfortunately, there are many border cases, so over time I've become tired of thinking about when I should use one kind of quotes or the other. So everything is in double quotes now. Also, I guess most people are rather used to double quotes for strings and thus will welcome the change. :-)
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 file 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.