# Copyright (C) 2003-2006, Stefan Schwarzer <sschwarzer@sschwarzer.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# - Neither the name of the above author nor the names of the
# contributors to the software may be used to endorse or promote
# products derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
ftp_file.py - support for file-like objects on FTP servers
"""
# $Id: ftp_file.py 679 2007-01-26 08:04:51Z schwa $
import ftp_error
# converter for `\r\n` line ends to normalized ones in Python. RFC 959
# states that the server will send `\r\n` on text mode transfers, so
# this conversion should be safe. I still use text mode transfers
# (mode 'r', not 'rb') in `socket.makefile` (below) because the
# server may do charset conversions on text transfers.
#
# Note that the "obvious" implementation of replacing "\r\n" with
# "\n" would fail, if "\r" (without "\n") occured at the end of the
# string `text`
_crlf_to_python_linesep = lambda text: text.replace('\r', '')
# converter for Python line ends into `\r\n`
_python_to_crlf_linesep = lambda text: text.replace('\n', '\r\n')
# helper class for xreadline protocol for ASCII transfers
#XXX maybe we can use the `xreadlines` module instead of this?
class _XReadlines(object):
"""Represents `xreadline` objects for ASCII transfers."""
def __init__(self, ftp_file):
self._ftp_file = ftp_file
self._next_index = 0
def __getitem__(self, index):
"""Return next line with specified index."""
if index != self._next_index:
raise RuntimeError( "_XReadline access index "
"out of order (expected %s but got %s)" %
(self._next_index, index) )
line = self._ftp_file.readline()
if not line:
raise IndexError("_XReadline object out of data")
self._next_index += 1
return line
class _FTPFile(object):
"""
Represents a file-like object connected to an FTP host. File and
socket are closed appropriately if the `close` operation is
requested.
"""
def __init__(self, host):
"""Construct the file(-like) object."""
self._host = host
self._session = host._session
# the file is closed yet
self.closed = True
def _open(self, path, mode):
"""Open the remote file with given path name and mode."""
# check mode
if 'a' in mode:
raise ftp_error.FTPIOError("append mode not supported")
if mode not in ('r', 'rb', 'w', 'wb'):
raise ftp_error.FTPIOError("invalid mode '%s'" % mode)
# remember convenience variables instead of mode
self._bin_mode = 'b' in mode
self._read_mode = 'r' in mode
# select ASCII or binary mode
transfer_type = ('A', 'I')[self._bin_mode]
command = 'TYPE %s' % transfer_type
ftp_error._try_with_ioerror(self._session.voidcmd, command)
# make transfer command
command_type = ('STOR', 'RETR')[self._read_mode]
command = '%s %s' % (command_type, path)
# ensure we can process the raw line separators;
# force to binary regardless of transfer type
if not 'b' in mode:
mode = mode + 'b'
# get connection and file object
self._conn = ftp_error._try_with_ioerror(
self._session.transfercmd, command)
self._fo = self._conn.makefile(mode)
# this comes last so that `close` does not try to
# close `_FTPFile` objects without `_conn` and `_fo`
# attributes
self.closed = False
#
# Read and write operations with support for line separator
# conversion for text modes.
#
# Note that we must convert line endings because the FTP server
# expects `\r\n` to be sent on text transfers.
#
def read(self, *args):
"""Return read bytes, normalized if in text transfer mode."""
data = self._fo.read(*args)
if self._bin_mode:
return data
data = _crlf_to_python_linesep(data)
if args == ():
return data
# If the read data contains `\r` characters the number of read
# characters will be too small! Thus we (would) have to
# continue to read until we have fetched the requested number
# of bytes (or run out of source data).
#
# The algorithm below avoids repetitive string concatanations
# in the style of
# data = data + more_data
# and so should also work relatively well if there are many
# short lines in the file.
wanted_size = args[0]
chunks = [data]
current_size = len(data)
while current_size < wanted_size:
# print 'not enough bytes (now %s, wanting %s)' % \
# (current_size, wanted_size)
more_data = self._fo.read(wanted_size - current_size)
if not more_data:
break
more_data = _crlf_to_python_linesep(more_data)
# print '-> new (normalized) data:', repr(more_data)
chunks.append(more_data)
current_size += len(more_data)
return ''.join(chunks)
def readline(self, *args):
"""Return one read line, normalized if in text transfer mode."""
data = self._fo.readline(*args)
if self._bin_mode:
return data
# if necessary, complete begun newline
if data.endswith('\r'):
data = data + self.read(1)
return _crlf_to_python_linesep(data)
def readlines(self, *args):
"""Return read lines, normalized if in text transfer mode."""
lines = self._fo.readlines(*args)
if self._bin_mode:
return lines
# more memory-friendly than `return [... for line in lines]`
for index, line in enumerate(lines):
lines[index] = _crlf_to_python_linesep(line)
return lines
def xreadlines(self):
"""
Return an appropriate `xreadlines` object with built-in line
separator conversion support.
"""
if self._bin_mode:
return self._fo.xreadlines()
return _XReadlines(self)
def __iter__(self):
"""Return a file iterator."""
return self
def next(self):
"""
Return the next line or raise `StopIteration`, if there are
no more.
"""
# apply implicit line ending conversion
line = self.readline()
if line:
return line
else:
raise StopIteration
def write(self, data):
"""Write data to file. Do linesep conversion for text mode."""
if not self._bin_mode:
data = _python_to_crlf_linesep(data)
self._fo.write(data)
def writelines(self, lines):
"""Write lines to file. Do linesep conversion for text mode."""
if self._bin_mode:
self._fo.writelines(lines)
return
# we can't modify the list of lines in-place, as in the
# `readlines` method; that would modify the original list,
# given as argument `lines`
for line in lines:
self._fo.write(_python_to_crlf_linesep(line))
#
# other attributes
#
def __getattr__(self, attr_name):
"""
Handle requests for attributes unknown to `_FTPFile` objects:
delegate the requests to the contained file object.
"""
if attr_name in ('flush isatty fileno seek tell '
'truncate name softspace'.split()):
return getattr(self._fo, attr_name)
raise AttributeError(
"'FTPFile' object has no attribute '%s'" % attr_name)
def close(self):
"""Close the `FTPFile`."""
if not self.closed:
self._fo.close()
ftp_error._try_with_ioerror(self._conn.close)
try:
ftp_error._try_with_ioerror(self._session.voidresp)
except ftp_error.FTPIOError, exception:
# ignore some errors, see ticket #17 at
# http://ftputil.sschwarzer.net/trac/ticket/17
error_code = str(exception).split()[0]
if error_code not in ("426", "450", "451"):
raise
self.closed = True
syntax highlighted by Code2HTML, v. 0.9.1