source: ftputil/file.py @ 1482:600fb1c435a9

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