source: ftputil/file.py @ 1196:04acdc17fd70

Last change on this file since 1196:04acdc17fd70 was 1196:04acdc17fd70, checked in by Stefan Schwarzer <sschwarzer@…>, 7 years ago
Use modern string formatting with `format` method.
File size: 9.3 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
9import ftputil.error
10
11
12# This module shouldn't be used by clients of the ftputil library.
13__all__ = []
14
15
16# Converter for `\r\n` line ends to normalized ones in Python. RFC 959
17# states that the server will send `\r\n` on text mode transfers, so
18# this conversion should be safe. I still use text mode transfers
19# (mode 'r', not 'rb') in `socket.makefile` (below) because the
20# server may do charset conversions on text transfers.
21#
22# Note that the "obvious" implementation of replacing "\r\n" with
23# "\n" would fail if "\r" (without "\n") occured at the end of the
24# string `text`.
25def _crlf_to_python_linesep(text):
26    """
27    Return `text` with ASCII line endings (CR/LF) converted to
28    Python's internal representation (LF).
29    """
30    return text.replace('\r', '')
31
32
33# Converter for Python line ends to `\r\n`
34def _python_to_crlf_linesep(text):
35    """
36    Return `text` with Python's internal line ending representation
37    (LF) converted to ASCII line endings (CR/LF).
38    """
39    return text.replace('\n', '\r\n')
40
41
42class _FTPFile(object):
43    """
44    Represents a file-like object associated with an FTP host. File
45    and socket are closed appropriately if the `close` operation is
46    called.
47    """
48
49    # Set timeout in seconds when closing file connections (see ticket #51).
50    _close_timeout = 5
51
52    def __init__(self, host):
53        """Construct the file(-like) object."""
54        self._host = host
55        self._session = host._session
56        # The file is still closed.
57        self.closed = True
58        # Overwritten later in `_open`.
59        self._bin_mode = None
60        self._conn = None
61        self._read_mode = None
62        self._fo = None
63
64    def _open(self, path, mode):
65        """Open the remote file with given path name and mode."""
66        # Check mode.
67        if 'a' in mode:
68            raise ftputil.error.FTPIOError("append mode not supported")
69        if mode not in ('r', 'rb', 'w', 'wb'):
70            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
71        # Remember convenience variables instead of the mode itself.
72        self._bin_mode = 'b' in mode
73        self._read_mode = 'r' in mode
74        # Select ASCII or binary mode.
75        transfer_type = ('A', 'I')[self._bin_mode]
76        command = 'TYPE {0}'.format(transfer_type)
77        with ftputil.error.ftplib_error_to_ftp_io_error:
78            self._session.voidcmd(command)
79        # Make transfer command.
80        command_type = ('STOR', 'RETR')[self._read_mode]
81        command = '{0} {1}'.format(command_type, path)
82        # Ensure we can process the raw line separators.
83        # Force to binary regardless of transfer type.
84        if not 'b' in mode:
85            mode = mode + 'b'
86        # Get connection and file object.
87        with ftputil.error.ftplib_error_to_ftp_io_error:
88            self._conn = self._session.transfercmd(command)
89        self._fo = self._conn.makefile(mode)
90        # This comes last so that `close` won't try to close `_FTPFile`
91        # objects without `_conn` and `_fo` attributes in case of an error.
92        self.closed = False
93
94    #
95    # Read and write operations with support for line separator
96    # conversion for text modes.
97    #
98    # Note that we must convert line endings because the FTP server
99    # expects `\r\n` to be sent on text transfers.
100    #
101    def read(self, *args):
102        """Return read bytes, normalized if in text transfer mode."""
103        data = self._fo.read(*args)
104        if self._bin_mode:
105            return data
106        data = _crlf_to_python_linesep(data)
107        if args == ():
108            return data
109        # If the read data contains `\r` characters the number of read
110        # characters will be too small! Thus we (would) have to
111        # continue to read until we have fetched the requested number
112        # of bytes (or run out of source data).
113        #
114        # The algorithm below avoids repetitive string concatanations
115        # in the style of
116        #     data = data + more_data
117        # and so should also work relatively well if there are many
118        # short lines in the file.
119        wanted_size = args[0]
120        chunks = [data]
121        current_size = len(data)
122        while current_size < wanted_size:
123            # print 'not enough bytes (now %s, wanting %s)' % \
124            #       (current_size, wanted_size)
125            more_data = self._fo.read(wanted_size - current_size)
126            if not more_data:
127                break
128            more_data = _crlf_to_python_linesep(more_data)
129            # print '-> new (normalized) data:', repr(more_data)
130            chunks.append(more_data)
131            current_size += len(more_data)
132        return ''.join(chunks)
133
134    def readline(self, *args):
135        """Return one read line, normalized if in text transfer mode."""
136        data = self._fo.readline(*args)
137        if self._bin_mode:
138            return data
139        # If necessary, complete begun newline.
140        if data.endswith('\r'):
141            data = data + self.read(1)
142        return _crlf_to_python_linesep(data)
143
144    def readlines(self, *args):
145        """Return read lines, normalized if in text transfer mode."""
146        lines = self._fo.readlines(*args)
147        if self._bin_mode:
148            return lines
149        # More memory-friendly than `return [... for line in lines]`
150        for index, line in enumerate(lines):
151            lines[index] = _crlf_to_python_linesep(line)
152        return lines
153
154    def __iter__(self):
155        """Return a file iterator."""
156        return self
157
158    def next(self):
159        """
160        Return the next line or raise `StopIteration`, if there are
161        no more.
162        """
163        # Apply implicit line ending conversion.
164        line = self.readline()
165        if line:
166            return line
167        else:
168            raise StopIteration
169
170    def write(self, data):
171        """Write data to file. Do linesep conversion for text mode."""
172        if not self._bin_mode:
173            data = _python_to_crlf_linesep(data)
174        self._fo.write(data)
175
176    def writelines(self, lines):
177        """Write lines to file. Do linesep conversion for text mode."""
178        if self._bin_mode:
179            self._fo.writelines(lines)
180            return
181        # We can't modify the list of lines in-place, as in the
182        # `readlines` method. That would modify the original list,
183        # given as argument `lines`.
184        for line in lines:
185            self._fo.write(_python_to_crlf_linesep(line))
186
187    #
188    # Context manager methods
189    #
190    def __enter__(self):
191        # Return `self`, so it can be accessed as the variable
192        # component of the `with` statement.
193        return self
194
195    def __exit__(self, exc_type, exc_val, exc_tb):
196        # We don't need the `exc_*` arguments here
197        # pylint: disable=W0613
198        self.close()
199        # Be explicit
200        return False
201
202    #
203    # Other attributes
204    #
205    def __getattr__(self, attr_name):
206        """
207        Handle requests for attributes unknown to `_FTPFile` objects:
208        delegate the requests to the contained file object.
209        """
210        if attr_name in ('flush isatty fileno seek tell '
211                         'truncate name softspace'.split()):
212            return getattr(self._fo, attr_name)
213        raise AttributeError(
214              "'FTPFile' object has no attribute '{0}'".format(attr_name))
215
216    # TODO: Implement `__dir__`? (See
217    # http://docs.python.org/py3k/whatsnew/2.6.html#other-language-changes )
218
219    def close(self):
220        """Close the `FTPFile`."""
221        if self.closed:
222            return
223        # Timeout value to restore, see below.
224        # Statement works only before the try/finally statement,
225        # otherwise Python raises an `UnboundLocalError`.
226        old_timeout = self._session.sock.gettimeout()
227        try:
228            self._fo.close()
229            self._fo = None
230            with ftputil.error.ftplib_error_to_ftp_io_error:
231                self._conn.close()
232            # Set a timeout to prevent waiting until server timeout
233            # if we have a server blocking here like in ticket #51.
234            self._session.sock.settimeout(self._close_timeout)
235            try:
236                with ftputil.error.ftplib_error_to_ftp_io_error:
237                    self._session.voidresp()
238            except ftputil.error.FTPIOError as exc:
239                # Ignore some errors, see tickets #51 and #17 at
240                # http://ftputil.sschwarzer.net/trac/ticket/51 and
241                # http://ftputil.sschwarzer.net/trac/ticket/17,
242                # respectively.
243                exc = str(exc)
244                error_code = exc[:3]
245                if exc.splitlines()[0] != "timed out" and \
246                  error_code not in ("150", "426", "450", "451"):
247                    raise
248        finally:
249            # Restore timeout for socket of `_FTPFile`'s `ftplib.FTP`
250            # object in case the connection is reused later.
251            self._session.sock.settimeout(old_timeout)
252            # If something went wrong before, the file is probably
253            # defunct and subsequent calls to `close` won't help
254            # either, so we consider the file closed for practical
255            # purposes.
256            self.closed = True
Note: See TracBrowser for help on using the repository browser.