root/trunk/ftp_file.py

Revision 764, 10.6 kB (checked in by schwa, 3 weeks ago)
Changed wording of deprecation message so users don't think they'll
have time forever. ;-)
  • 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-2008, 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 warnings
39
40 import ftp_error
41
42
43 # converter for `\r\n` line ends to normalized ones in Python. RFC 959
44 #  states that the server will send `\r\n` on text mode transfers, so
45 #  this conversion should be safe. I still use text mode transfers
46 #  (mode 'r', not 'rb') in `socket.makefile` (below) because the
47 #  server may do charset conversions on text transfers.
48 #
49 # Note that the "obvious" implementation of replacing "\r\n" with
50 #  "\n" would fail, if "\r" (without "\n") occured at the end of the
51 #  string `text`
52 def _crlf_to_python_linesep(text):
53     """
54     Return `text` with ASCII line endings (CR/LF) converted to
55     Python's internal representation (LF).
56     """
57     return text.replace('\r', '')
58
59 # converter for Python line ends into `\r\n`
60 def _python_to_crlf_linesep(text):
61     """
62     Return `text` with Python's internal line ending representation
63     (LF) converted to ASCII line endings (CR/LF).
64     """
65     return text.replace('\n', '\r\n')
66
67
68 # helper class for xreadline protocol for ASCII transfers
69 #XXX maybe we can use the `xreadlines` module instead of this?
70 class _XReadlines(object):
71     """Represents `xreadline` objects for ASCII transfers."""
72
73     def __init__(self, ftp_file):
74         self._ftp_file = ftp_file
75         self._next_index = 0
76
77     def __getitem__(self, index):
78         """Return next line with specified index."""
79         if index != self._next_index:
80             raise RuntimeError( "_XReadline access index "
81                   "out of order (expected %s but got %s)" %
82                   (self._next_index, index) )
83         line = self._ftp_file.readline()
84         if not line:
85             raise IndexError("_XReadline object out of data")
86         self._next_index += 1
87         return line
88
89
90 class _FTPFile(object):
91     """
92     Represents a file-like object connected to an FTP host. File and
93     socket are closed appropriately if the `close` operation is
94     requested.
95     """
96
97     def __init__(self, host):
98         """Construct the file(-like) object."""
99         self._host = host
100         self._session = host._session
101         # the file is closed yet
102         self.closed = True
103         # overwritten later in `_open`
104         self._bin_mode = None
105         self._conn = None
106         self._read_mode = None
107         self._fo = None
108
109     def _open(self, path, mode):
110         """Open the remote file with given path name and mode."""
111         # check mode
112         if 'a' in mode:
113             raise ftp_error.FTPIOError("append mode not supported")
114         if mode not in ('r', 'rb', 'w', 'wb'):
115             raise ftp_error.FTPIOError("invalid mode '%s'" % mode)
116         # remember convenience variables instead of mode
117         self._bin_mode = 'b' in mode
118         self._read_mode = 'r' in mode
119         # select ASCII or binary mode
120         transfer_type = ('A', 'I')[self._bin_mode]
121         command = 'TYPE %s' % transfer_type
122         ftp_error._try_with_ioerror(self._session.voidcmd, command)
123         # make transfer command
124         command_type = ('STOR', 'RETR')[self._read_mode]
125         command = '%s %s' % (command_type, path)
126         # ensure we can process the raw line separators;
127         #  force to binary regardless of transfer type
128         if not 'b' in mode:
129             mode = mode + 'b'
130         # get connection and file object
131         self._conn = ftp_error._try_with_ioerror(
132                      self._session.transfercmd, command)
133         self._fo = self._conn.makefile(mode)
134         # this comes last so that `close` does not try to
135         #  close `_FTPFile` objects without `_conn` and `_fo`
136         #  attributes
137         self.closed = False
138
139     #
140     # Read and write operations with support for line separator
141     # conversion for text modes.
142     #
143     # Note that we must convert line endings because the FTP server
144     # expects `\r\n` to be sent on text transfers.
145     #
146     def read(self, *args):
147         """Return read bytes, normalized if in text transfer mode."""
148         data = self._fo.read(*args)
149         if self._bin_mode:
150             return data
151         data = _crlf_to_python_linesep(data)
152         if args == ():
153             return data
154         # If the read data contains `\r` characters the number of read
155         #  characters will be too small! Thus we (would) have to
156         #  continue to read until we have fetched the requested number
157         #  of bytes (or run out of source data).
158         #
159         # The algorithm below avoids repetitive string concatanations
160         #  in the style of
161         #      data = data + more_data
162         #  and so should also work relatively well if there are many
163         #  short lines in the file.
164         wanted_size = args[0]
165         chunks = [data]
166         current_size = len(data)
167         while current_size < wanted_size:
168             # print 'not enough bytes (now %s, wanting %s)' % \
169             #       (current_size, wanted_size)
170             more_data = self._fo.read(wanted_size - current_size)
171             if not more_data:
172                 break
173             more_data = _crlf_to_python_linesep(more_data)
174             # print '-> new (normalized) data:', repr(more_data)
175             chunks.append(more_data)
176             current_size += len(more_data)
177         return ''.join(chunks)
178
179     def readline(self, *args):
180         """Return one read line, normalized if in text transfer mode."""
181         data = self._fo.readline(*args)
182         if self._bin_mode:
183             return data
184         # if necessary, complete begun newline
185         if data.endswith('\r'):
186             data = data + self.read(1)
187         return _crlf_to_python_linesep(data)
188
189     def readlines(self, *args):
190         """Return read lines, normalized if in text transfer mode."""
191         lines = self._fo.readlines(*args)
192         if self._bin_mode:
193             return lines
194         # more memory-friendly than `return [... for line in lines]`
195         for index, line in enumerate(lines):
196             lines[index] = _crlf_to_python_linesep(line)
197         return lines
198
199     def xreadlines(self):
200         """
201         Return an appropriate `xreadlines` object with built-in line
202         separator conversion support.
203         """
204         warnings.warn(("FTPFile.xreadlines is deprecated and will be removed "
205           "in ftputil 2.5"), DeprecationWarning, stacklevel=2)
206         if self._bin_mode:
207             return self._fo.xreadlines()
208         return _XReadlines(self)
209
210     def __iter__(self):
211         """Return a file iterator."""
212         return self
213
214     def next(self):
215         """
216         Return the next line or raise `StopIteration`, if there are
217         no more.
218         """
219         # apply implicit line ending conversion
220         line = self.readline()
221         if line:
222             return line
223         else:
224             raise StopIteration
225
226     def write(self, data):
227         """Write data to file. Do linesep conversion for text mode."""
228         if not self._bin_mode:
229             data = _python_to_crlf_linesep(data)
230         self._fo.write(data)
231
232     def writelines(self, lines):
233         """Write lines to file. Do linesep conversion for text mode."""
234         if self._bin_mode:
235             self._fo.writelines(lines)
236             return
237         # we can't modify the list of lines in-place, as in the
238         #  `readlines` method; that would modify the original list,
239         #  given as argument `lines`
240         for line in lines:
241             self._fo.write(_python_to_crlf_linesep(line))
242
243     #
244     # context manager methods
245     #
246     def __enter__(self):
247         # return `self`, so it can be accessed as the variable
248         #  component of the `with` statement.
249         return self
250
251     def __exit__(self, exc_type, exc_val, exc_tb):
252         # we don't need the `exc_*` arguments here
253         # pylint: disable-msg=W0613
254         self.close()
255         # be explicit
256         return False
257
258     #
259     # other attributes
260     #
261     def __getattr__(self, attr_name):
262         """
263         Handle requests for attributes unknown to `_FTPFile` objects:
264         delegate the requests to the contained file object.
265         """
266         if attr_name in ('flush isatty fileno seek tell '
267                          'truncate name softspace'.split()):
268             return getattr(self._fo, attr_name)
269         raise AttributeError(
270               "'FTPFile' object has no attribute '%s'" % attr_name)
271
272     def close(self):
273         """Close the `FTPFile`."""
274         if self.closed:
275             return
276         try:
277             self._fo.close()
278             ftp_error._try_with_ioerror(self._conn.close)
279             try:
280                 ftp_error._try_with_ioerror(self._session.voidresp)
281             except ftp_error.FTPIOError, exception:
282                 # ignore some errors, see ticket #17 at
283                 #  http://ftputil.sschwarzer.net/trac/ticket/17
284                 error_code = str(exception).split()[0]
285                 if error_code not in ("426", "450", "451"):
286                     raise
287         finally:
288             # if something went wrong before, the file is probably
289             #  defunct and subsequent calls to `close` won't help
290             #  either, so we consider the file closed for practical
291             #  purposes
292             self.closed = True
293
Note: See TracBrowser for help on using the browser.