root/tags/release2_2/ftp_file.py

Revision 647, 9.4 kB (checked in by schwa, 2 years ago)
Turn classes into new-style classes by letting them inherit from
`object`.
  • Property svn:mime-type set to text/x-python
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 # Copyright (C) 2003-2006, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 # - Redistributions of source code must retain the above copyright
9 #   notice, this list of conditions and the following disclaimer.
10 #
11 # - Redistributions in binary form must reproduce the above copyright
12 #   notice, this list of conditions and the following disclaimer in the
13 #   documentation and/or other materials provided with the distribution.
14 #
15 # - Neither the name of the above author nor the names of the
16 #   contributors to the software may be used to endorse or promote
17 #   products derived from this software without specific prior written
18 #   permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
24 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32 """
33 ftp_file.py - support for file-like objects on FTP servers
34 """
35
36 # $Id$
37
38 import ftp_error
39
40
41 # converter for `\r\n` line ends to normalized ones in Python. RFC 959
42 #  states that the server will send `\r\n` on text mode transfers, so
43 #  this conversion should be safe. I still use text mode transfers
44 #  (mode 'r', not 'rb') in `socket.makefile` (below) because the
45 #  server may do charset conversions on text transfers.
46 #
47 # Note that the "obvious" implementation of replacing "\r\n" with
48 #  "\n" would fail, if "\r" (without "\n") occured at the end of the
49 #  string `text`
50 _crlf_to_python_linesep = lambda text: text.replace('\r', '')
51
52 # converter for Python line ends into `\r\n`
53 _python_to_crlf_linesep = lambda text: text.replace('\n', '\r\n')
54
55
56 # helper class for xreadline protocol for ASCII transfers
57 #XXX maybe we can use the `xreadlines` module instead of this?
58 class _XReadlines(object):
59     """Represents `xreadline` objects for ASCII transfers."""
60     def __init__(self, ftp_file):
61         self._ftp_file = ftp_file
62         self._next_index = 0
63
64     def __getitem__(self, index):
65         """Return next line with specified index."""
66         if index != self._next_index:
67             raise RuntimeError( "_XReadline access index "
68                   "out of order (expected %s but got %s)" %
69                   (self._next_index, index) )
70         line = self._ftp_file.readline()
71         if not line:
72             raise IndexError("_XReadline object out of data")
73         self._next_index += 1
74         return line
75
76
77 class _FTPFile(object):
78     """
79     Represents a file-like object connected to an FTP host. File and
80     socket are closed appropriately if the `close` operation is
81     requested.
82     """
83     def __init__(self, host):
84         """Construct the file(-like) object."""
85         self._host = host
86         self._session = host._session
87         # the file is closed yet
88         self.closed = True
89
90     def _open(self, path, mode):
91         """Open the remote file with given path name and mode."""
92         # check mode
93         if 'a' in mode:
94             raise ftp_error.FTPIOError("append mode not supported")
95         if mode not in ('r', 'rb', 'w', 'wb'):
96             raise ftp_error.FTPIOError("invalid mode '%s'" % mode)
97         # remember convenience variables instead of mode
98         self._bin_mode = 'b' in mode
99         self._read_mode = 'r' in mode
100         # select ASCII or binary mode
101         transfer_type = ('A', 'I')[self._bin_mode]
102         command = 'TYPE %s' % transfer_type
103         ftp_error._try_with_ioerror(self._session.voidcmd, command)
104         # make transfer command
105         command_type = ('STOR', 'RETR')[self._read_mode]
106         command = '%s %s' % (command_type, path)
107         # ensure we can process the raw line separators;
108         #  force to binary regardless of transfer type
109         if not 'b' in mode:
110             mode = mode + 'b'
111         # get connection and file object
112         self._conn = ftp_error._try_with_ioerror(
113                      self._session.transfercmd, command)
114         self._fo = self._conn.makefile(mode)
115         # this comes last so that `close` does not try to
116         #  close `_FTPFile` objects without `_conn` and `_fo`
117         #  attributes
118         self.closed = False
119
120     #
121     # Read and write operations with support for line separator
122     # conversion for text modes.
123     #
124     # Note that we must convert line endings because the FTP server
125     # expects `\r\n` to be sent on text transfers.
126     #
127     def read(self, *args):
128         """Return read bytes, normalized if in text transfer mode."""
129         data = self._fo.read(*args)
130         if self._bin_mode:
131             return data
132         data = _crlf_to_python_linesep(data)
133         if args == ():
134             return data
135         # If the read data contains `\r` characters the number of read
136         #  characters will be too small! Thus we (would) have to
137         #  continue to read until we have fetched the requested number
138         #  of bytes (or run out of source data).
139         #
140         # The algorithm below avoids repetitive string concatanations
141         #  in the style of
142         #      data = data + more_data
143         #  and so should also work relatively well if there are many
144         #  short lines in the file.
145         wanted_size = args[0]
146         chunks = [data]
147         current_size = len(data)
148         while current_size < wanted_size:
149             # print 'not enough bytes (now %s, wanting %s)' % \
150             #       (current_size, wanted_size)
151             more_data = self._fo.read(wanted_size - current_size)
152             if not more_data:
153                 break
154             more_data = _crlf_to_python_linesep(more_data)
155             # print '-> new (normalized) data:', repr(more_data)
156             chunks.append(more_data)
157             current_size += len(more_data)
158         return ''.join(chunks)
159
160     def readline(self, *args):
161         """Return one read line, normalized if in text transfer mode."""
162         data = self._fo.readline(*args)
163         if self._bin_mode:
164             return data
165         # if necessary, complete begun newline
166         if data.endswith('\r'):
167             data = data + self.read(1)
168         return _crlf_to_python_linesep(data)
169
170     def readlines(self, *args):
171         """Return read lines, normalized if in text transfer mode."""
172         lines = self._fo.readlines(*args)
173         if self._bin_mode:
174             return lines
175         # more memory-friendly than `return [... for line in lines]`
176         for index, line in enumerate(lines):
177             lines[index] = _crlf_to_python_linesep(line)
178         return lines
179
180     def xreadlines(self):
181         """
182         Return an appropriate `xreadlines` object with built-in line
183         separator conversion support.
184         """
185         if self._bin_mode:
186             return self._fo.xreadlines()
187         return _XReadlines(self)
188
189     def __iter__(self):
190         """Return a file iterator."""
191         return self
192
193     def next(self):
194         """
195         Return the next line or raise `StopIteration`, if there are
196         no more.
197         """
198         # apply implicit line ending conversion
199         line = self.readline()
200         if line:
201             return line
202         else:
203             raise StopIteration
204
205     def write(self, data):
206         """Write data to file. Do linesep conversion for text mode."""
207         if not self._bin_mode:
208             data = _python_to_crlf_linesep(data)
209         self._fo.write(data)
210
211     def writelines(self, lines):
212         """Write lines to file. Do linesep conversion for text mode."""
213         if self._bin_mode:
214             self._fo.writelines(lines)
215             return
216         # we can't modify the list of lines in-place, as in the
217         #  `readlines` method; that would modify the original list,
218         #  given as argument `lines`
219         for line in lines:
220             self._fo.write(_python_to_crlf_linesep(line))
221
222     #
223     # other attributes
224     #
225     def __getattr__(self, attr_name):
226         """
227         Handle requests for attributes unknown to `_FTPFile` objects:
228         delegate the requests to the contained file object.
229         """
230         if attr_name in ('flush isatty fileno seek tell '
231                          'truncate name softspace'.split()):
232             return getattr(self._fo, attr_name)
233         raise AttributeError(
234               "'FTPFile' object has no attribute '%s'" % attr_name)
235
236     def close(self):
237         """Close the `FTPFile`."""
238         if not self.closed:
239             self._fo.close()
240             ftp_error._try_with_ioerror(self._conn.close)
241             try:
242                 ftp_error._try_with_ioerror(self._session.voidresp)
243             except ftp_error.FTPIOError, exception:
244                 # ignore some errors, see ticket #17 at
245                 #  http://ftputil.sschwarzer.net/trac/ticket/17
246                 error_code = str(exception).split()[0]
247                 if error_code not in ("426", "450"):
248                     raise
249             self.closed = True
250
Note: See TracBrowser for help on using the browser.