Index: ftputil.py
===================================================================
--- ftputil.py (revision 0:9db916ed2259)
+++ ftputil.py (revision 1:e93155993afc)
@@ -31,46 +31,71 @@
 
 import ftplib
-import cStringIO
+
 
 class FTPIOError(IOError):
-    pass
+    def __init__(self, msg, ftp_exception=None):
+        self.ftp_exception = ftp_exception
+        IOError(self, msg)
+
 
 class _FTPFile:
     '''Represents a file-like object connected to an
-    FTP host.'''
+    FTP host. File and socket are closed appropriately if
+    the close operation is requested.'''
 
-    def __init__(self, host, path):
+    def __init__(self, conn, mode):
         '''Construct the file(-like) object.'''
-        self._host = host
-        self._path = path
-        self.closed = 0
+        self._conn = conn
+        # this should be returned if someone asks
+        self.mode = mode
+        self._binary = 'b' in mode
+        if mode == 'r':
+            # the FTP server ensures the correct mode via
+            #  the previous TYPE command
+            mode = 'rb'
+        self._fp = conn.makefile(mode)
         
-    def read(self, bytes=None):
-        '''Return 'bytes' bytes from the file object, or up
-        to EOF if the argument is not provided.'''
-        pass
+    def _normalize_linefeeds(text):
+        r'''Return data with occurences of \r removed.'''
+        return data.replace('\r', '')
 
-    def readline(self):
-        '''Return a single line from the file object.'''
-        pass
+    def read(self, *args, **kwargs):
+        '''Return read bytes, normalized if in ASCII
+        transfer mode.'''
+        text = apply(self._fp.read, args, kwargs)
+        if self._binary:
+            return text
+        else:
+            return self._normalize_linefeeds(text)
 
-    def readlines(self):
-        '''Return a list of lines until EOF.'''
-        pass
+    def readlines(self, *args, **kwargs):
+        '''Return read lines, normalized if in ASCII
+        transfer mode.'''
+        lines = apply(self._fp.readlines, args, kwargs)
+        return [self._normalize_linefeeds(line)
+                for line in lines]
+        
+    def __getattr__(self, attr_name):
+        '''Delegate unknown attribute requests to the file.'''
+        if attr_name in ( 'flush isatty fileno read readline '
+          'readlines xreadlines seek tell truncate write '
+          'writelines closed name softspace'.split() ):
+            return eval('self._fp.%s' % attr_name)
+        else:
+            raise AttributeError("'FTPFile' object has no "
+                  "attribute '%s'" % attr_name)
+        
+    def close(self):
+        '''Close the FTPFile. We need no 'if'; the file and the
+        socket object can be closed multiply without harm.'''
+        self._fp.close()
+        self._conn.close()
 
-    def write(self, data):
-        '''Write a data string to the file.'''
-        pass
+    def __del__(self):
+        # not strictly necessary; file and socket are
+        #  closed on garbage collection, anyway
+        self.close()
 
-    def writelines(self, data):
-        '''Write a list of strings to the file object.'''
-        pass
 
-    def close(self):
-        if not self.closed:
-            # what will we do?
-            pass
-
-    
 class FTPHost:
     '''FTP host class'''
@@ -80,25 +105,32 @@
         stage I don't know if I need a new FTP connection for
         each file transfer.'''
-        self._hostname = host
+        self._hostname = hostname
         self._user = user
         self._password = password
-        self._host = ftplib.FTP(hostnamme, user, password)
+        self._host = ftplib.FTP(hostname, user, password)
     
     def file(self, path, mode='r'):
-        '''Return a file-like object that is connected to
+        '''Return a file(-like) object that is connected to an
         FTP host.'''
-        # we don't support append modes
-        assert '+' not in mode
-        if 'r' in mode:
-            # read modes
-            if 'b' in mode:
-                # open for binary reading
-            else:
-                # open for ASCII reading
-        elif 'w' in mode:
-            # write modes
-            if 'b' in mode:
-                # open for binary writing
-            else:
-                # open for ASCII writing
+        if '+' in mode:
+            raise FTPIOError("append modes not supported")
+        if mode not in ( 'r rb br w wb bw'.split() ):
+            raise FTPIOError("invalid mode")
+        # select ASCII or binary mode
+        transfer_type = ('A', 'I')['b' in mode]
+        command = 'TYPE %s' % transfer_type
+        # logic taken from ftplib;
+        #  why this strange distinction?
+        if mode == 'r':
+            self._host.sendcmd(command)
+        else:
+            self._host.voidcmd(command)
+        # make transfer command
+        command_type = ('STOR', 'RETR')['r' in mode]
+        command = '%s %s' % (command_type, path)
+        # get connection and file object
+        conn = self._host.transfercmd(command)
+        self._host.voidresp()
+        ftp_file = _FTPFile(conn, mode)
+        return ftp_file
 
