#!/usr/bin/env python
# sftp-ui.py - a script around sftp
#
# Requires: Twisted Python framework for networking, sftp command
# Author: Tuukka Hastrup <Tuukka@iki.fi>
# License: GNU General Public License
# URL:
#
# Usage: sftp-ui.py [user@]host path
#        (works similarily to: scp -r user@host:path/* .)
# Example: mkdir public_html-backup ; cd public_html-backup
#          sftp-ui.py john@www.domain.example public_html
#
# The script will start sftp logged to the given host, cd to the given path,
# and download all files and directories recursively to the current dir.
#
# ChangeLog:
# 2004-05-11: BFS->DFS, alphabetical order
# 2004-05-11: removed debug output
# 2004-05-11: initial working version
#
# TODO:
# * implement sftp and scp semantics
# * files with spaces or newlines, quotes
# * command-line options
# * implement other entries than files and dirs (links!)
# * more features than just recursive download
# * GUI

from twisted.internet import protocol # you'll need python-twisted
from twisted.internet import reactor
import sys

def debug(label,msg):
    lines = msg.split('\n')
    for line in lines[:-1]:
        print "%s: %s" % (label,line)

    if lines[-1] != '':
        print "%s: %s\\" % (label,lines[-1])


class SFTPProtocol(protocol.ProcessProtocol):
    def __init__(self, path):
        self.lsl = "ls -l"

        self.path = path # root path for our recursive download
        self.data = ""   # left-over incomplete output from line-wise receive
        self.commands = [] # queued commands
        self.handlers = [] # queued command result handlers
        self.cmdresult = None # command result gathered
        self.quitting = False # whether we are done with commands

    def printhandler(self, result):
        """General handler that simply prints the command result output."""
        if result != []: print result
    def lshandler(self, dir, result):
        print "Processing dir '%s'." % dir
        commands = []
        handlers = []
        #result.sort(lambda x,y:cmp(x.split(' ')[-1],
        #                           y.split(' ')[-1])) # alphabetical order
        for line in result:
            #name = line.split(' ')[-1]
            name = line[line.find('.'):]
            try:
                sort = {'-': 'file', 'd': 'dir', 'l': 'link'}[line[0]]
            except:
                sort = 'unknown'
            #pathi = "%s%s" % (dir, name)
            pathi = name
            if name.split('/')[-1] == '.' or name.split('/')[-1] == '..':
                continue
            # print 'entry: %s (%s)' % (pathi, sort)
            if sort == 'file':
                commands.append('get "%s" "%s"' % (pathi,pathi))
                handlers.append(self.printhandler)
            elif sort == 'dir':
                # print "pathi:", pathi
                commands.append('lmkdir "%s"' % pathi)
                handlers.append(self.printhandler)
                commands.append('%s "%s"' % (self.lsl,pathi))
                # XXX why aren't these two equivalent? python bug?
                #handlers.append((lambda result:self.lshandler(pathi+'/',result)))
                handlers.append((lambda dir:lambda result:self.lshandler(dir,result))(pathi+'/'))
            else:
                print "XXX files of type %s not handled" % sort
        # prepend for stack-like and depth-first operation
        self.commands = commands + self.commands 
        self.handlers = handlers + self.handlers
        
    def connectionMade(self):
        """called when the process is started. Queues the first command."""
        print "Child process launched."
        self.commands.append('cd "%s"' % self.path)
        self.handlers.append(self.printhandler)
        self.commands.append('%s "."' % self.lsl)
        self.handlers.append(lambda x:self.lshandler('',x))
    def outReceived(self, data):
        """called when we receive data from the process's stdout"""
        #print "outReceived! with %d bytes!" % len(data)
        #debug("out",data)
        lines = (self.data + data).split('\n')
        for line in lines[:-1]: # handle complete lines
            self.cmdresult.append(line)
            #debug("line", line)
            
        if lines[-1] == 'sftp> ':
            self.data = '' # no left-over, this handles prompt
            if self.cmdresult != None: # not first prompt
                self.handlers.pop(0)(self.cmdresult)
            self.cmdresult = [] # start gathering new result
            try:
                command = self.commands.pop(0)+'\n'
                self.transport.write(command)
                #debug("in",command)
            except IndexError:
                print "No more commands, closing stdin..."
                self.quitting = True
                self.transport.closeStdin() # let's exit
        else:
            self.data = lines[-1] # left-over
            
        
    def errReceived(self, data):
        #print "errReceived! with %d bytes!" % len(data)
        debug("err",data)
    def inConnectionLost(self):
        if not self.quitting:
            print "The child closed its stdin!"
    def outConnectionLost(self):
        if not self.quitting:
            print "The child closed its stdout!"
    def errConnectionLost(self):
        if not self.quitting:
            print "The child closed its stderr!"
    def processEnded(self, status_object):
        code = status_object.value.exitCode
        if code == 0:
            print "Child process completed without failure."
        else:
            print "Child process ended with status code %d." % code
        reactor.stop()


if __name__ == '__main__':
    if len(sys.argv) != 3:
        print "Usage: sftp-ui.py [user@]host path"
        sys.exit(5)
    login = sys.argv[1]
    path = sys.argv[2]
    p = SFTPProtocol(path)
    cmd = ["sftp",login]
    #cmd = ["ssh","itu","sftp",login]
    print "Launching child process: %s" % ' '.join(cmd)
    reactor.spawnProcess(p, cmd[0], cmd, {})
    reactor.run()

