source: ftputil/file.py @ 1619:66ef713f4fe8

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