Programming Forums
User Name Password Register
 

RSS Feed
FORUM INDEX | TODAY'S POSTS | UNANSWERED THREADS | ADVANCED SEARCH

Reply
 
Thread Tools Display Modes
Old Jun 22nd, 2006, 9:39 PM   #1
Sane
Programming Guru
 
Sane's Avatar
 
Join Date: Apr 2005
Location: Waterloo, Ontario
Posts: 2,187
Rep Power: 6 Sane will become famous soon enough
Send a message via MSN to Sane
Python FTP Client Wrapper

Perhaps, those of you who are learning Python, might like some code to look at. Others, this could be very useful for you if you are trying to communicate with an FTP.

This is a sufficiently documented wrapper to the operations of an FTP. It is all set up so it becomes very simple to create your own code that completes tasks on an FTP server.

"""
====================================================
              FTP Client Command API
====================================================

  >>> May 26 2006
  >>> dr.sane@gmail.com, drsane_@hotmail.com

This program may NOT be used for commerical purposes
    without prior permission from the author!

Copyright Aaron Voelker 2006
          http://saney.ath.cx/
          
====================================================
"""


import socket
import sys
import os
import time
import urllib2

UNEXPECTED_RESPONSE = '100'
MISSING_PAGE        = '200'

""" handle the incoming messages from the server """
class handler:
    
    def get_between(self, t, s, e):
        return t.split(s)[-1].split(e)[0]

    def get_id(self, msg):
        lines = msg.split('\r\n')
        return int(lines[-2].split(' ')[0])

    def parse_pasv(self, msg):
        nmsg = self.get_between(msg, '(', ')')
        p = nmsg.split(',')
        return '.'.join(p[:4]), int(p[4])*256 + int(p[5])

    def parse_nlst(self, msg):
        if msg:
            return msg.split('\r\n')[:-1]
        else:
            return []

    def global_parse(self, msg):
        return self.get_between(msg, ' ', '\r\n')

    def string_to_epoch(self, t, timezone):
        localtime = (int(t[:4]),
                     int(t[4:6]),
                     int(t[6:8]),int(t[8:10]),int(t[10:12]),int(t[12:14]),
                     0,0,0)
        return time.mktime(localtime)+timezone

    def parse_time(self, msg, timezone):
        string = self.global_parse(msg)
        return self.string_to_epoch(string, timezone)

    def parse_size(self, msg):
        return self.global_parse(msg)

    def validify_case(self, msg):
        """basic exceptions to some of the relays,
        this will validify if some reponses were
        appropriate enough to continue as required"""
       
        if self.get_id(msg) in (450, 550):
            # 450: no files found in folder
            # 550: means a folder (not a file)
            return False
        return True


""" make a custom socket object """
class mk_socket:
    
    def __init__(self, sid, host, port=21):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((host, port))
        self.sid = str(sid)
        self.open = True
        self.handle = handler()

    def hold_state(self, error):
        # in the case of an error...
        #print '\n\n!!!', error
        # raw_input('    Socket ID: %s\nPress any key to terminate program ...\n'%self.sid)
        #sys.exit(1)
        pass

    def cls(self):
        # don't close the socket more than once
        if self.open:
            self.s.close()
            self.open = False

    def relay(self, mes='', expect=False, filt=''):
        self.send(mes, True, filt)
        return self.recv(expect)

    def recv(self, expect=False):
        print self.sid, '<<<',
        
        try:
            rec = self.s.recv(1024)
        except socket.error:
            self.hold_state('Software caused connection abort')
            
        print rec

        # use expect

        if len(rec) > 3:
            # server has another message
            if rec[3] == '-':
                return rec+self.recv()
        return rec        

    def send(self, mes='', CRLF=True, filt=''):
        print self.sid, '>>>',

        try:
            self.s.send(mes + ('', '\r\n')[CRLF==True])
            # '\r\n' is a <CRLF> (endline), this tells the 
            # server that this is the end of the command
        except socket.error:
            self.hold_state('Connection reset')

        if CRLF: # avoid outputting the file transfers
            if filt:
                print mes.replace(filt, '*'*len(filt))
            else:
                print mes


""" open a file, 1024 bytes at a time """
class open_slow:

    def __init__(self, name):

        self.f = open(name, 'rb')
        self.size = os.stat(name)[6]
        self.pos = 0
        self.opened = True

    def next(self, buff=1024):

        self.f.seek(self.pos)
        self.pos += buff

        if self.pos >= self.size:
            piece = self.f.read(-1)
            self.f.close()
            self.opened = False
        else:
            piece = self.f.read(buff)
            
        return piece

"""
FTP Client Class
----------------

This class, for communicating with an FTP,
consists of the following functions ...

.connect       connect to the host
.think         output a thought to the console (debugging)
.do_nothing    check for a connection
.upload        upload a file by its name and contents
.set_time      synchronize the time with the server
               looks for a page containing:
               <?php echo time(); ?>
.set_timezone  set the timezone by hours off of server
               
.LOGIN         login to the FTP with a username and password
.DIR           get a list of files
.SIZE          get the size of a file in bytes
.TIME          get the time of modification in seconds since epoch
.CDUP          change to parent directory
.CWD           change the current working directory
.MKD           create a new folder
.MODE          set the transfer mode
.TYPE          set the type of file to be transferred
.STRU          set the file structure for transfer
.PASV          a passive connection will have the data
               transfer port open on the server's side
.QUIT          close session and connections
"""

class ftp_client:

    # initiate the ftp client class
    def __init__(self):
        # initiate the reply handler
        self.handle = handler()

        # timesonze is defaulted to zero
        self.timezone = 0

        # buffer size
        self.buffer_size = 1024

        # so we know it doesn't exist yet in self.PASV()
        self.sock_pasv = False

    # external helpers
    def connect(self, host):
        """connect to the host"""
        self.sock_main = mk_socket(1, host)
        self.sock_main.recv()

    def set_time(self, page):
        """synchronize the time with the server"""
        ip, port = self.PASV()
        try:
            self.think('Synchronizing time...')
            opener = urllib2.build_opener()
            t1 = int(opener.open('http://'+ip+'/'+page).read())
            t2 = time.time()
            self.timezone = t2 - (t1 + 3600*5)
            self.think('Time zone set to %s seconds'%self.timezone)
            time.sleep(0.5)
            
        except Exception, msg:
            self.error(MISSING_PAGE, 'was unable to synchronize time')

        self.sock_pasv.cls() # close the passive port

    def set_timezone(self, h):
        """set the timezone by hours off of server"""
        self.timezone = h*3600
                                 
    # start temporary functions
    def think(self, thought):
        print "!!!", str(thought), '\n'

    def do_nothing(self):
        """check for a connection"""
        self.sock_main.relay('NOOP')

    def error(self, err, msg):
        raise err

    # start command related functions
    def LOGIN(self, usern, passw):
        """login to the FTP with a username and password"""
        self.sock_main.relay('USER '+usern)
        res = self.sock_main.relay('PASS '+passw, filt=passw)

        # expect a 230 (user logged in)
        if self.handle.get_id(res) != 230:
            self.error(UNEXPECTED_RESPONSE, 'incorrect username or password')
            
    def DIR(self):
        """get a list of files"""

        self.PASV()
        msg = self.sock_main.relay('NLST')
        
        if self.handle.validify_case(msg):

            msg = ''
            add = True
            while add != '':
                add = self.sock_pasv.recv()
                msg += add
            self.think('Empty message sent. File list is done.')
            # there is a CRLF, we know we can close
            self.sock_pasv.cls()
        
            flist = self.handle.parse_nlst(msg)
            self.think(flist)
        
            # trigger the expected 226
            self.sock_main.recv(226)

        else:
            self.sock_pasv.cls()
            flist = [] # no files = an empty list
        
        return flist
        
    def SIZE(self, f):
        """get the size of a file in bytes"""
        msg = self.sock_main.relay('SIZE '+f)
        if self.handle.validify_case(msg):
            fsize = self.handle.parse_size(msg)
        else:
            fsize = False
        return fsize

    def TIME(self, f):
        """get the time of modification in seconds since epoch"""
        msg = self.sock_main.relay('MDTM '+f)
        if self.handle.validify_case(msg):
            ftime = self.handle.parse_time(msg, self.timezone)
        else:
            ftime = False
        return ftime

    def CDUP(self):
        """change to parent directory"""
        # note: untested
        self.sock_main.relay('CDUP')

    def MODE(self, m='S'):
        """set the transfer mode"""
        self.sock_main.relay('MODE '+m)

    def TYPE(self, t='A'):
        """set the type of file to be transferred"""
        self.sock_main.relay('TYPE '+t)

    def STRU(self, s='F'):
        """set the file structure for transfer"""
        self.sock_main.relay('STRU '+s)
        
    def CWD(self, dname):
        """change the current working directory"""
        self.sock_main.relay('CWD '+dname, 250)

    def MKD(self, dname):
        """create a new folder"""
        self.sock_main.relay('MKD '+dname, 257)

    def PASV(self):
        """a passive connection will have the data
        transfer port open on the server's side"""

        if self.sock_pasv:
            self.think('Checking for open socket')
            assert not self.sock_pasv.open # make sure there is no port open
        
        # propose passive connection
        msg = self.sock_main.relay('PASV')
        newip, newport = self.handle.parse_pasv(msg)

        # make passive connection
        self.sock_pasv = mk_socket(2, newip, newport)

        return newip, newport # return the passive IP/PORT

    def QUIT(self):
        """close session and connections"""
        if self.sock_pasv:
            if self.sock_pasv.open:
                self.think('Passive port open... closing')
                self.sock_pasv.cls()
            else:
                self.think('Passive port already closed')
        self.sock_main.relay('QUIT')
        self.sock_main.cls()
        
    # start quick-access functions
    def upload(self, fname, fsource):
        """upload a file by its name and contents"""

        # open passive connection
        self.PASV()
        self.sock_main.relay('STOR '+fname)

        my_file = open_slow(fsource)
        while my_file.opened:
            self.sock_pasv.send(my_file.next(self.buffer_size),
                                False) # don't include the CRLF in the file transfer

        # close passive connection to end transfer
        self.sock_pasv.cls()
        # warning: the passive port is now closed!

        # trigger the expected 226
        self.sock_main.recv(226)

    # break upload up in to managable pieces
    # for more customizable access
    def upload_init(self, fname, fsource):
        self.PASV()
        self.sock_main.relay('STOR '+fname)
        my_file = open_slow(fsource)
        return my_file
    
    def upload_send(self, my_file, buff):
        self.sock_pasv.send(my_file.next(buff), False)
        return my_file.opened

    def upload_abort(self):
        self.sock_main.send('ABOR')
        self.sock_pasv.cls()
        self.sock_main.recv(226)

    def upload_close(self):
        self.sock_pasv.cls()
        self.sock_main.recv(226)


if __name__ == '__main__':
    """example usage of the FTP Client class"""

    MYclient = ftp_client()
    
    try:
        MYclient.connect(raw_input("Enter FTP Address: "))
        
    except socket.error:
        print "Could not connect"
        sys.exit(1)
        
    try:
        MYclient.LOGIN(raw_input("Enter Full Username: "),
                       raw_input("Enter Password: "))
        
    except UNEXPECTED_RESPONSE:
        print "Incorrect login"
        sys.exit(1)
    
    MYclient.MODE()    # stream transfer mode
    MYclient.TYPE('I') # image/binary type
    MYclient.STRU()    # file structure

    #MYclient.set_time('~omega/time.php')

    MYclient.CWD('/')
    MYclient.MKD('test')
    MYclient.CWD('test')

    dest = 'my_test_file.py'
    MYclient.upload(dest, sys.argv[0])
    
    folder = MYclient.DIR()
    fsize = MYclient.SIZE(dest)
    ftime = MYclient.TIME(dest)

    MYclient.QUIT()

    if dest in folder:
        print "Success! '%s' uploaded to the folder.\n\n Size: %s BYTES\n Time: %s"%(dest,
                                                                               fsize,
                                                                               time.ctime(ftime))
    else:
        print "File did not succesfully upload."

    #print folder

The ending section, 'if __name__ == "__main__"...' is a demonstration of what you can build with the client class.

Feel free to extend off of the example, or complete more of the FTP functions. Currently, it only has what you would need to upload a file. It will be very simple to include more FTP operations (just google FTP command codes).

Tell me what you think. =]
Sane is online now   Reply With Quote
Old Jun 22nd, 2006, 10:11 PM   #2
Yegg
Newbie
 
Join Date: Jan 2006
Posts: 20
Rep Power: 0 Yegg is on a distinguished road
Maybe I missed something, but Python has an ftplib module. Why not use that?
Yegg is offline   Reply With Quote
Old Jun 22nd, 2006, 10:22 PM   #3
Sane
Programming Guru
 
Sane's Avatar
 
Join Date: Apr 2005
Location: Waterloo, Ontario
Posts: 2,187
Rep Power: 6 Sane will become famous soon enough
Send a message via MSN to Sane
I think the main difference is mine is simplified to do all the necessary stuff in one shot. Instead of making you figure out the commands required to prepare the subsequent ones.

A minor difference that could easily be solved in your own wrapper around ftplib, had I known of its existance. Haha.

It was still a fun problem solving exercise.
Sane is online now   Reply With Quote
Old Jun 22nd, 2006, 10:26 PM   #4
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 655
Rep Power: 4 Jessehk is on a distinguished road
Maybe I'm just in a bad mood (perhaps jealousy from reading your site? ), but I notice a lot of naming inconsistencies. Stick to one convention.
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!
Jessehk is offline   Reply With Quote
Old Jun 22nd, 2006, 10:31 PM   #5
Sane
Programming Guru
 
Sane's Avatar
 
Join Date: Apr 2005
Location: Waterloo, Ontario
Posts: 2,187
Rep Power: 6 Sane will become famous soon enough
Send a message via MSN to Sane
Well... my naming conventions are ... mostly for me. Every name has a reason for how its titled. Like constants that are only used for read access, I make all upper case. Translated functions are all upper case. Most others are all lower case with underscores. Like I said, there's reasons behind all of it. And it's all there to help me.
Sane is online now   Reply With Quote
Old Jun 22nd, 2006, 10:42 PM   #6
Jessehk
The Oblivious One
 
Jessehk's Avatar
 
Join Date: May 2005
Location: Ontario, Canada
Posts: 655
Rep Power: 4 Jessehk is on a distinguished road
Quote:
Originally Posted by Sane
Well... my naming conventions are ... mostly for me. Every name has a reason for how its titled. Like constants that are only used for read access, I make all upper case. Translated functions are all upper case. Most others are all lower case with underscores. Like I said, there's reasons behind all of it. And it's all there to help me.
It may work for you, but it would be hard to follow for somebody attempting to use your code like a library.
__________________
Dr. Zoidberg: [ecstatic] I'm going to a movie... with FRIENDS!
Jessehk is offline   Reply With Quote
Old Jun 22nd, 2006, 10:44 PM   #7
Sane
Programming Guru
 
Sane's Avatar
 
Join Date: Apr 2005
Location: Waterloo, Ontario
Posts: 2,187
Rep Power: 6 Sane will become famous soon enough
Send a message via MSN to Sane
Ahh. I see exactly what you mean. Well, easy fixes these are. ^^
Sane is online now   Reply With Quote
Reply

Bookmarks

« Previous Thread in Forum | Next Thread in Forum »

Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Forum Jump




DaniWeb IT Discussion Community
All times are GMT -5. The time now is 2:16 PM.

Powered by vBulletin® Version 3.7.0, Copyright ©2000 - 2009, Jelsoft Enterprises Ltd.
Copyright ©2007 DaniWeb® LLC