You are not logged in.

#1 2009-05-19 03:08:48

Peasantoid
Member
Registered: 2009-04-26
Posts: 928
Website

Reinventing the wheel: Clutter (file server written in Python)

I got bored and decided to write a file server.

Server:

#!/usr/bin/env python
import optparse, os, socket, select, signal, sys

e_ok =        0
e_emptyreq =  1
e_notfile =   2
e_other =     3

fsize_len = 16 # length of file size word

def err(msg):
    sys.stderr.write(msg + '\n')
    sys.exit(1)

# send a message with appropriate status code
def sendmsg(sock, code, msg):
    sock.sendall('{0}{1}'.format(chr(code), msg))
    if code > e_ok:
        sock.close()

# turn an integer into a series of octets -- maximum value is 2^128 - 1
def makefsize(size):
    i = 0
    word = ''
    while i < fsize_len:
        word = '{0}{1}'.format(chr(size & 0xFF), word)
        size >>= 8
        i += 1
    return word

def main():
    parser = optparse.OptionParser(usage = 'Usage: %prog <options>')
    parser.add_option('-d', '--docroot', dest = 'docroot', action = 'store', default = os.path.expanduser('~'), help = 'directory to serve files from [~]')
    parser.add_option('-p', '--port', dest = 'port', action = 'store', type = 'int', default = 6666, help = 'port to listen on [%default]')
    parser.add_option('--allow-dotfiles', dest = 'allow_dotfiles', action = 'store_true', default = False, help = 'allow clients to access dotfiles [no]')
    parser.add_option('--send-blocksize', dest = 'send_blocksize', action = 'store', type = 'int', default = 4096, help = 'number of bytes to send at a time [%default]')
    parser.add_option('--connect-backlog', dest = 'connect_backlog', action = 'store', type = 'int', default = 3, help = 'backlog for connect() [%default]')
    opts, args = parser.parse_args()
    
    master = socket.socket(socket.AF_INET, socket.SOCK_STREAM);
    master.bind(('localhost', opts.port))
    master.listen(opts.connect_backlog);
    os.chroot(opts.docroot)
    signal.signal(signal.SIGCHLD, signal.SIG_IGN) # let the system auto-reap zombie processes

    while True:
        select.select([master], [], []) # not sure what to use here; something in {poll}, maybe?
        
        # fire-and-forget a new process to handle the request
        if os.fork() == 0:
            client = master.accept()
            print('Incoming connection from {0}:{1}'.format(client[1][0], client[1][1]))
            
            # hmm, this just sits there and blocks if the client requests an empty file
            req = client[0].recv(4096).rstrip() # get rid of trailing newline
            if len(req) == 0:
                sendmsg(client[0], e_emptyreq, 'Empty request')
                sys.exit(1)

            if not os.path.isfile(req) or (not opts.allow_dotfiles and os.path.basename(req)[0] == '.'):
                sendmsg(client[0], e_notfile, 'Not a file')
                sys.exit(1)
            try:
                fp = open(req, 'rb')
            except IOError, e:
                sendmsg(client[0], e_other, str(e))
                sys.exit(1)

            # send info: status code + file size
            client[0].sendall('{0}{1}'.format(chr(e_ok), makefsize(os.stat(req).st_size)))
            
            while 1:
                read = fp.read(opts.send_blocksize)
                if read == '': # at EOF, terminate client
                    fp.close()
                    client[0].close()
                    break
                client[0].sendall(read)

            sys.exit(1) # done with this process
    master.close()
try: main()
except: raise

Client:

#!/usr/bin/env python
import optparse, os, socket, sys

e_ok = 0

fsize_len = 16 # length of file size word

def err(msg, fatal = False):
    sys.stderr.write(msg + '\n')
    if fatal: sys.exit(1)

# turn a series of octets into an integer
def getfsize(word):
    if len(word) != fsize_len:
        err('Bad file size')
    size = i = 0
    while i < fsize_len:
        size <<= 8
        size |= ord(word[i])
        i += 1
    return size

def main():
    parser = optparse.OptionParser(usage = 'Usage: %prog [options] [file1] [file2] ...')
    parser.add_option('-s', '--server', dest = 'server', action = 'store', default = 'localhost', help = 'server to download files from [%default]')
    parser.add_option('-p', '--port', dest = 'port', action = 'store', default = 6666, type = 'int',  help = 'port on server [%default]')
    parser.add_option('-d', '--dl-dir', dest = 'dldir', action = 'store', default = '.',  help = 'directory to place downloaded files in [%default]')
    parser.add_option('--recv-blocksize', dest = 'recv_blocksize', action = 'store', default = 4096, type = 'int', help = 'number of bytes to receive at a time [%default]')
    opts, filenames = parser.parse_args()    

    for filename in filenames:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((opts.server, opts.port))

        print('Requesting file {0}'.format(filename))
        s.sendall(filename)

        inf = s.recv(1 + fsize_len) # status code + file size
        if len(inf) == 0:
            err('Empty response')
            continue
        code = ord(inf[0])
        inf = inf[1:]
        if code != e_ok:
            err('Error: ' + inf)
            continue
        if len(inf) != fsize_len:
            err('Server sent invalid file information')
            continue
        
        already_exists = False
        filename = opts.dldir + '/' + os.path.basename(filename)
        print('Destination: {0}'.format(filename))
        while os.path.exists(filename):
            already_exists = True
            filename += '_'
        if already_exists: print('Already exists, writing to: {0}'.format(filename))
        fp = open(filename, 'wb')

        size = getfsize(inf)
        rsize = 0
        read = ''
        sys.stdout.write('\033[?25l') # hide the cursor
        while 1:
            sys.stdout.write('\033[sDownloading... {0}/{1} {2}%        \033[u'
                .format(rsize, size, int((float(rsize) / float(size)) * 100)))
            read = s.recv(opts.recv_blocksize)
            if read == '': break
            rsize += len(read)
            fp.write(read)

        fp.close()
        s.close()
        print('\033[?25h\nDone.') # show the cursor

try: main()
except:
    sys.stdout.write('\033[?25h') # restore the cursor if something went wrong
    raise

Can has feedback? smile

(Note: I'm in the process of reworking clutter from an earlier (read: worse) design, so please point out anything useless in the code.)

Last edited by Peasantoid (2009-05-19 23:11:35)

Offline

#2 2009-05-19 12:19:09

Heller_Barde
Member
Registered: 2008-04-01
Posts: 245

Re: Reinventing the wheel: Clutter (file server written in Python)

sooo, this is basically a read only ftp client-server application without the ftp?

neat big_smile

Offline

#3 2009-05-19 14:02:45

techprophet
Member
Registered: 2008-05-13
Posts: 209

Re: Reinventing the wheel: Clutter (file server written in Python)

Nice and compact. I'll try it out this afternoon.

Offline

#4 2009-05-19 14:12:02

tomd123
Developer
Registered: 2008-08-12
Posts: 565

Re: Reinventing the wheel: Clutter (file server written in Python)

nice and minimal... great reference for python beginners. ^^

Offline

#5 2009-05-19 15:43:35

genisis300
Member
From: Uk
Registered: 2008-01-15
Posts: 284

Re: Reinventing the wheel: Clutter (file server written in Python)

lol i was just about to start working on something like this.


"is adult entertainment killing our children or is killing our children entertaining adults?" Marilyn Manson

Offline

#6 2009-05-19 19:38:31

Peasantoid
Member
Registered: 2009-04-26
Posts: 928
Website

Re: Reinventing the wheel: Clutter (file server written in Python)

Hey thanks, guys. smile Wasn't expecting to get such positive feedback — it's my first major Python project.

I plan to add command-line option support soon, once I figure out Python's getopt module.

Offline

#7 2009-05-19 20:39:19

genisis300
Member
From: Uk
Registered: 2008-01-15
Posts: 284

Re: Reinventing the wheel: Clutter (file server written in Python)

just the thing for you then smile

Let me know how you get on smile

http://www.alexonlinux.com/pythons-optp … man-beings


"is adult entertainment killing our children or is killing our children entertaining adults?" Marilyn Manson

Offline

#8 2009-05-19 22:00:29

Peasantoid
Member
Registered: 2009-04-26
Posts: 928
Website

Re: Reinventing the wheel: Clutter (file server written in Python)

genisis300 wrote:

just the thing for you then smile

Let me know how you get on smile

http://www.alexonlinux.com/pythons-optp … man-beings

Thanks, that was great!

On a related note, I've updated the code.

Offline

#9 2009-05-20 13:41:51

keenerd
Package Maintainer (PM)
Registered: 2007-02-22
Posts: 647
Website

Re: Reinventing the wheel: Clutter (file server written in Python)

whoops, missed you code updates.  Feel free to delete.

Last edited by keenerd (2009-05-20 13:43:48)

Offline

#10 2009-05-20 19:34:44

Peasantoid
Member
Registered: 2009-04-26
Posts: 928
Website

Re: Reinventing the wheel: Clutter (file server written in Python)

Now has a half-assed project page over at my website (link). Will be AUR-ing this soon.

Offline

#11 2009-05-20 20:36:15

Peasantoid
Member
Registered: 2009-04-26
Posts: 928
Website

Re: Reinventing the wheel: Clutter (file server written in Python)

AUR'd. (link)

# Note that it's "clutter-ft", not "clutter" — that's been taken.

Last edited by Peasantoid (2009-05-20 20:36:37)

Offline

Board footer

Powered by FluxBB