source: ftputil/file.py @ 1715:01215a325738

Last change on this file since 1715:01215a325738 was 1713:f146a1ea66aa, checked in by Stefan Schwarzer <sschwarzer@…>, 10 months ago
Remove `__future__` imports With the switch to Python 3.x-only, the `__future__` imports are no longer needed. Update copyright years along with the `__future__` import removal.
File size: 7.7 KB
Line 
1# Copyright (C) 2003-2018, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
3# See the file LICENSE for licensing terms.
4
5"""
6ftputil.file - support for file-like objects on FTP servers
7"""
8
9import io
10
11import ftputil.compat
12import ftputil.error
13import ftputil.socket_file_adapter
14
15
16# This module shouldn't be used by clients of the ftputil library.
17__all__ = []
18
19
20class FTPFile(object):
21    """
22    Represents a file-like object associated with an FTP host. File
23    and socket are closed appropriately if the `close` method is
24    called.
25    """
26
27    # Set timeout in seconds when closing file connections (see ticket #51).
28    _close_timeout = 5
29
30    def __init__(self, host):
31        """Construct the file(-like) object."""
32        self._host = host
33        # pylint: disable=protected-access
34        self._session = host._session
35        # The file is still closed.
36        self.closed = True
37        self._conn = None
38        self._fobj = None
39
40    def _open(self, path, mode, buffering=None, encoding=None, errors=None,
41              newline=None, rest=None):
42        """
43        Open the remote file with given path name and mode.
44
45        Contrary to the `open` builtin, this method returns `None`,
46        instead this file object is modified in-place.
47        """
48        # We use the same arguments as in `io.open`.
49        # pylint: disable=too-many-arguments
50        #
51        # `buffering` argument isn't used at this time.
52        # pylint: disable=unused-argument
53        #
54        # Python 3's `socket.makefile` supports the same interface as
55        # the new `open` builtin, but Python 2 supports only a mode,
56        # but doesn't return an object with the proper interface to
57        # wrap it in `io.TextIOWrapper`.
58        #
59        # Therefore, to make the code work on Python 2 _and_ 3, use
60        # `socket.makefile` to always create a binary file and under
61        # Python 2 wrap it in an adapter class.
62        #
63        # Check mode.
64        if "a" in mode:
65            raise ftputil.error.FTPIOError("append mode not supported")
66        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
67            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
68        if "b" in mode and "t" in mode:
69            # Raise a `ValueError` like Python would.
70            raise ValueError("can't have text and binary mode at once")
71        # Convenience variables
72        is_binary_mode = "b" in mode
73        is_read_mode = "r" in mode
74        # `rest` is only allowed for binary mode.
75        if (not is_binary_mode) and (rest is not None):
76            raise ftputil.error.CommandNotImplementedError(
77                    "`rest` argument can't be used for text files")
78        # Always use binary mode (see comments above).
79        transfer_type = "I"
80        command = "TYPE {0}".format(transfer_type)
81        with ftputil.error.ftplib_error_to_ftp_io_error:
82            self._session.voidcmd(command)
83        # Make transfer command.
84        command_type = "RETR" if is_read_mode else "STOR"
85        command = "{0} {1}".format(command_type, path)
86        # Force to binary regardless of transfer type (see above).
87        makefile_mode = mode
88        makefile_mode = makefile_mode.replace("t", "")
89        if not "b" in makefile_mode:
90            makefile_mode += "b"
91        # Get connection and file object.
92        with ftputil.error.ftplib_error_to_ftp_io_error:
93            self._conn = self._session.transfercmd(command, rest)
94        # The file object. Under Python 3, this will already be a
95        # `BufferedReader` or `BufferedWriter` object.
96        fobj = self._conn.makefile(makefile_mode)
97        if ftputil.compat.python_version == 2:
98            BufferedIOAdapter = ftputil.socket_file_adapter.BufferedIOAdapter
99            if is_read_mode:
100                fobj = BufferedIOAdapter(fobj, is_readable=True)
101            else:
102                fobj = BufferedIOAdapter(fobj, is_writable=True)
103        if not is_binary_mode:
104            fobj = io.TextIOWrapper(fobj, encoding=encoding,
105                                    errors=errors, newline=newline)
106        self._fobj = fobj
107        # This comes last so that `close` won't try to close `FTPFile`
108        # objects without `_conn` and `_fobj` attributes in case of an
109        # error.
110        self.closed = False
111
112    def __iter__(self):
113        """Return a file iterator."""
114        return self
115
116    def __next__(self):
117        """
118        Return the next line or raise `StopIteration`, if there are
119        no more.
120        """
121        # Apply implicit line ending conversion for text files.
122        line = self.readline()
123        if line:
124            return line
125        else:
126            raise StopIteration
127
128    # Although Python 2.6+ has the `next` builtin function already, it
129    # still requires iterators to have a `next` method.
130    next = __next__
131
132    #
133    # Context manager methods
134    #
135    def __enter__(self):
136        # Return `self`, so it can be accessed as the variable
137        # component of the `with` statement.
138        return self
139
140    def __exit__(self, exc_type, exc_val, exc_tb):
141        # We don't need the `exc_*` arguments here
142        # pylint: disable=unused-argument
143        self.close()
144        # Be explicit
145        return False
146
147    #
148    # Other attributes
149    #
150    def __getattr__(self, attr_name):
151        """
152        Handle requests for attributes unknown to `FTPFile` objects:
153        delegate the requests to the contained file object.
154        """
155        if attr_name in ("encoding flush isatty fileno read readline "
156                         "readlines seek tell truncate name softspace "
157                         "write writelines".split()):
158            return getattr(self._fobj, attr_name)
159        raise AttributeError(
160                "'FTPFile' object has no attribute '{0}'".format(attr_name))
161
162    # TODO: Implement `__dir__`? (See
163    # http://docs.python.org/whatsnew/2.6.html#other-language-changes )
164
165    def close(self):
166        """Close the `FTPFile`."""
167        if self.closed:
168            return
169        # Timeout value to restore, see below.
170        # Statement works only before the try/finally statement,
171        # otherwise Python raises an `UnboundLocalError`.
172        old_timeout = self._session.sock.gettimeout()
173        try:
174            self._fobj.close()
175            self._fobj = None
176            with ftputil.error.ftplib_error_to_ftp_io_error:
177                self._conn.close()
178            # Set a timeout to prevent waiting until server timeout
179            # if we have a server blocking here like in ticket #51.
180            self._session.sock.settimeout(self._close_timeout)
181            try:
182                with ftputil.error.ftplib_error_to_ftp_io_error:
183                    self._session.voidresp()
184            except ftputil.error.FTPIOError as exc:
185                # Ignore some errors, see tickets #51 and #17 at
186                # http://ftputil.sschwarzer.net/trac/ticket/51 and
187                # http://ftputil.sschwarzer.net/trac/ticket/17,
188                # respectively.
189                exc = str(exc)
190                error_code = exc[:3]
191                if exc.splitlines()[0] != "timed out" and \
192                  error_code not in ("150", "426", "450", "451"):
193                    raise
194        finally:
195            # Restore timeout for socket of `FTPFile`'s `ftplib.FTP`
196            # object in case the connection is reused later.
197            self._session.sock.settimeout(old_timeout)
198            # If something went wrong before, the file is probably
199            # defunct and subsequent calls to `close` won't help
200            # either, so we consider the file closed for practical
201            # purposes.
202            self.closed = True
203
204    def __getstate__(self):
205        raise TypeError("cannot serialize FTPFile object")
Note: See TracBrowser for help on using the repository browser.