source: ftputil/file.py @ 1335:1d93da752e9d

Last change on this file since 1335:1d93da752e9d was 1335:1d93da752e9d, checked in by Stefan Schwarzer <sschwarzer@…>, 7 years ago
Removed no longer used method `_wrapped_file`.
File size: 9.1 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.x's `socket.makefile` supports the same interface
131        # as the new `open` builtin, but Python 2.x supports a mode,
132        # but neither buffering nor encoding/decoding. Therefore, to
133        # make the code work on Python 2.x _and_ 3.x, create an
134        # unbuffered binary file and possibly wrap it.
135        #
136        # Check mode.
137        if "a" in mode:
138            raise ftputil.error.FTPIOError("append mode not supported")
139        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
140            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
141        if "b" in mode and "t" in mode:
142            # Raise a `ValueError` like Python would.
143            raise ValueError("can't have text and binary mode at once")
144        # Convenience variables
145        is_bin_mode = "b" in mode
146        is_read_mode = "r" in mode
147        # Always use binary mode (see above).
148        transfer_type = "I"
149        command = 'TYPE {0}'.format(transfer_type)
150        with ftputil.error.ftplib_error_to_ftp_io_error:
151            self._session.voidcmd(command)
152        # Make transfer command.
153        command_type = ('STOR', 'RETR')[is_read_mode]
154        command = '{0} {1}'.format(command_type, path)
155        # Force to binary regardless of transfer type (see above).
156        makefile_mode = mode
157        makefile_mode = makefile_mode.replace("t", "")
158        if not "b" in makefile_mode:
159            makefile_mode += "b"
160        # Get connection and file object.
161        with ftputil.error.ftplib_error_to_ftp_io_error:
162            self._conn = self._session.transfercmd(command)
163        # The actual file object. Under Python 3, this will already
164        # be wrapped by a `BufferedReader` or `BufferedWriter`.
165        fobj = self._conn.makefile(makefile_mode)
166        if ftputil.compat.python_version == 2:
167            if is_read_mode:
168                fobj = BufferedReaderWriter(fobj, is_readable=True)
169            else:
170                fobj = BufferedReaderWriter(fobj, is_writable=True)
171        if not is_bin_mode:
172            fobj = io.TextIOWrapper(fobj, encoding=encoding,
173                                    errors=errors, newline=newline)
174        self._fobj = fobj
175        # This comes last so that `close` won't try to close `_FTPFile`
176        # objects without `_conn` and `_fo` attributes in case of an error.
177        self.closed = False
178
179    def __iter__(self):
180        """Return a file iterator."""
181        return self
182
183    def __next__(self):
184        """
185        Return the next line or raise `StopIteration`, if there are
186        no more.
187        """
188        # Apply implicit line ending conversion.
189        line = self.readline()
190        if line:
191            return line
192        else:
193            raise StopIteration
194
195    # Although Python 2.6+ has the `next` builtin function already, it
196    # still requires iterators to have a `next` method.
197    next = __next__
198
199    #
200    # Context manager methods
201    #
202    def __enter__(self):
203        # Return `self`, so it can be accessed as the variable
204        # component of the `with` statement.
205        return self
206
207    def __exit__(self, exc_type, exc_val, exc_tb):
208        # We don't need the `exc_*` arguments here
209        # pylint: disable=W0613
210        self.close()
211        # Be explicit
212        return False
213
214    #
215    # Other attributes
216    #
217    def __getattr__(self, attr_name):
218        """
219        Handle requests for attributes unknown to `_FTPFile` objects:
220        delegate the requests to the contained file object.
221        """
222        if attr_name in ("encoding flush isatty fileno read readline "
223                         "readlines seek tell truncate name softspace "
224                         "write writelines".split()):
225            return getattr(self._fobj, attr_name)
226        raise AttributeError(
227              "'FTPFile' object has no attribute '{0}'".format(attr_name))
228
229    # TODO: Implement `__dir__`? (See
230    # http://docs.python.org/py3k/whatsnew/2.6.html#other-language-changes )
231
232    def close(self):
233        """Close the `FTPFile`."""
234        if self.closed:
235            return
236        # Timeout value to restore, see below.
237        # Statement works only before the try/finally statement,
238        # otherwise Python raises an `UnboundLocalError`.
239        old_timeout = self._session.sock.gettimeout()
240        try:
241            self._fobj.close()
242            self._fobj = None
243            with ftputil.error.ftplib_error_to_ftp_io_error:
244                self._conn.close()
245            # Set a timeout to prevent waiting until server timeout
246            # if we have a server blocking here like in ticket #51.
247            self._session.sock.settimeout(self._close_timeout)
248            try:
249                with ftputil.error.ftplib_error_to_ftp_io_error:
250                    self._session.voidresp()
251            except ftputil.error.FTPIOError as exc:
252                # Ignore some errors, see tickets #51 and #17 at
253                # http://ftputil.sschwarzer.net/trac/ticket/51 and
254                # http://ftputil.sschwarzer.net/trac/ticket/17,
255                # respectively.
256                exc = str(exc)
257                error_code = exc[:3]
258                if exc.splitlines()[0] != "timed out" and \
259                  error_code not in ("150", "426", "450", "451"):
260                    raise
261        finally:
262            # Restore timeout for socket of `_FTPFile`'s `ftplib.FTP`
263            # object in case the connection is reused later.
264            self._session.sock.settimeout(old_timeout)
265            # If something went wrong before, the file is probably
266            # defunct and subsequent calls to `close` won't help
267            # either, so we consider the file closed for practical
268            # purposes.
269            self.closed = True
Note: See TracBrowser for help on using the repository browser.