########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Ftp/Session.py,v 1.15 2005/04/11 16:51:12 jkloth Exp $
"""
Repository exposed as FTP session

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import string, socket, types, sha, time
import os, sys, thread, cStringIO, traceback
from Ft.Lib import Time
from Ft.Server.Common import XmlLib

from Ft.Server.Server import SCore, FtServerServerException, Error
from Ft.Server.Common import ResourceTypes, Schema
from cStringIO import StringIO

from Ft.Server.ThirdParty.pyftpd import timeout_socket
from Ft.Server.ThirdParty.pyftpd.config import *
from Ft.Lib import Uri

from Ft.Server import SCHEMA_NSS

from Ft.Server.Common.Util import IMT_MAP

from Ft.Rdf.Parsers import Versa
from Ft.Server.Server.Drivers import FtssInputSource


t_socket = timeout_socket.timeout_socket


authmethods = []
permmethods = []

sessions = {}

for i in modules:
    mod = __import__('Ft.Server.ThirdParty.pyftpd.'+i, {}, {}, ["*"])
    methods = dir(mod)
    if "got_user" in methods: # it is authentification module
        authmethods.append( (getattr(mod,'got_user'),
                             getattr(mod,'got_pass')))
    if "permcheck" in methods: # permission module
        permmethods.append(getattr(mod,"permcheck"))
    if "v2fs" in methods: # virtual to real path
        v2fs = getattr(mod,'v2fs') # this overrides v2fs ftom utils

def permcheck(f, user, group, session, operation):
    last = 0
    for i in permmethods:
        l, c = i(f, user, group, session, operation)
        if l == 0:
            last = 0
        elif l == 1:
            last = 1
        if not c:
            break
    return last


def got_user(username, session, sessions): # session points to Session class
    last_deny_rt = 500, "", "", "", 0, 0
    # response code, response message, user, group, deny_or_grant, continue
    last_grant_rt = 200, "", "", "", 0, 0
    last_rt = 500, "", "", "", 0, 0
    last = -1
    for i in authmethods:
        rt = n, r, u, g, l, c = i[0](username, session, sessions)
        if l == 0:
            last_deny_rt = rt
            last = 0
        elif l == 1:
            last_grant_rt = rt
            last = 1
        else:
            last_rt = rt
        if not c:
            break
    #now last is 1 for grant access, 0 for deny
    if last == 1:
        n, r, u, g, l, c = last_grant_rt
    elif last == 0:
        n, r, u, g, l, c = last_deny_rt
    else:
        n, r, u, g, l, c = last_rt
    return n, r, u, g, l

def got_pass(username, password, session, sessions):
    last_deny_rt = 500, "", 0, 0
    last_grant_rt = 200, "", 0, 0
    last_rt = 500, "", 0, 0
    last = 0
    for i in authmethods:
        rt = n, r, l, c = i[1](username, password, session, sessions)
        if l == 0:
            last_deny_rt = rt
            last = 0
        elif l == 1:
            last_grant_rt = rt
            last = 1
        else:
            last_rt = rt
        if not c:
            break
    #now last is 1 for grant access, 0 for deny
    if last == 1:
        n, r, l, c = last_grant_rt
    elif last == 0:
        n, r, l, c = last_deny_rt
    else:
        n, r, l, c = last_rt
    return n, r, l


class Session:
    threading = 0
    def __init__(self, rfile, wfile, client_address, server):
        self.serverAddy = server.hostname
        self.user = ""
        self.group = ""
        self.curcmd = "just connected"
        self.exit_immediately = 0
        self.pendingconn = 0 # if there is an open data connection - for ABOR
        self.last_cmd_time = self.session_create_time = time.time()
        self.rfile = rfile
        self.wfile = wfile
        self.restpos = 0
        self.passive = 0
        self.create_datasock = self.create_nonpasv_datasock
        #self.create_datasock = self.create_pasv_datasock
        self.ip = client_address[0]
        self.dataport = None
        self.replymessage(220, initial_msg)
        self.cwd = '/'
        self.limit_retr_speed = 0.
        self.limit_stor_speed = 0.

        #Additions
        self._userName = None
        self._password = None
        self.logFile = server.errorLog
        self.properties = server.properties
        self.extendedMapping = {}
        for ext, imt in IMT_MAP.items():
            self.extendedMapping[ext] = (None, None, imt)
        if hasattr(server,'extendedMapping'):
            self.extendedMapping.update(server.extendedMapping)
        
        self.cmddict = {
#            "rnfr": self.cmd_rnfr,
#            "rnto": self.cmd_rnto,
            "quit": self.cmd_quit,  #done
            "syst": self.cmd_syst,
            "user": self.cmd_user,  #done
            "pass": self.cmd_pass,  #done
            "port": self.cmd_port,  #done
            "stor": self.cmd_stor,  #done
            "appe": self.cmd_appe,  #done
            "dele": self.cmd_dele,  #done
            "mkd" : self.cmd_mkd,   #done
            "rmd" : self.cmd_rmd,   #done
            "retr": self.cmd_retr,  #done
            "rest": self.cmd_rest,  #done
            "size": self.cmd_size,  #done
            "list": self.cmd_list,  #done
            "nlst": self.cmd_list,  #done
            "pasv": self.cmd_pasv,  
            "pwd" : self.cmd_pwd,   #done
            "cwd" : self.cmd_cwd,   #done
            "cdup": self.cmd_cdup,  #done
            "site": self.cmd_site,
            "abor": self.cmd_abor,
            "type": self.cmd_dummy,
            "noop": self.cmd_dummy, #done
            }

    def v2fs(self, f):
        return v2fs(f, self)

    def reply(self, x):
        self.wfile.write(x + "\r\n")

    def replymessage(self, n, x): # reply to the client, x is possibly tuple of lines
        if type(x) == types.StringType:
            self.reply(`n`+" "+x)
        else:
            for i in x[:-1]:
                self.reply(`n`+"-"+i)
            self.reply(`n`+" "+x[-1])

    def create_nonpasv_datasock(self):
        if not self.dataport:
            self.reply("425 what about a PORT command?")
            return
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock = t_socket(sock=sock, timeout=timeout_data)
            self.sock.connect((self.ip, self.dataport))
            self.reply("150 dataconnection is up!")
            return 1
        except socket.error:
            self.reply("425 your dataport sucks big time!")
            return None

    def create_pasv_datasock(self):
        try:            
            self.sock = t_socket(sock=self.sock, timeout=timeout_data)
            conn, addr = self.sock.accept()            
            self.sock = t_socket(sock=conn, timeout=timeout_data)            
            self.reply("150 dataconnection is up!")
            self.create_datasock = self.create_nonpasv_datasock
            return 1
        except "socket.error":            
            self.reply("425 cannot create socket")
            return None        

    def close_datasock(self):
        self.sock.close()

    def cmd_quit(self,_):
        self.reply("221 Have a good one!")

    def cmd_abor(self,_):
        if self.pendingconn and self.threading:
            self.abort_received = 1
            while self.pendingconn:
                time.sleep(0.1) # prevent from taking up 100% CPU
        self.reply("226 Aborted")
        return

    def cmd_user(self, username):
        n, r, self.user, self.group, self.logged = got_user(username, self, sessions)
        self.replymessage(n, r)

    def cmd_pass(self, password):
        n, r, self.logged = got_pass(self.user, password, self, sessions)
        if self.logged:
            self._userName = self.user
            self._password = sha.new(password).hexdigest()
        self.replymessage(n, r)

    def cmd_dummy(self, _):
        self.reply("200 OK (in other words: ignored)")

    def cmd_syst(self, _):
        self.reply("215 UNIX Type: L8")

    def cmd_pwd(self, _):
        self.reply('257 "%s" is where you are' % self.cwd)

    def cmd_cdup(self, _):
        """
        Overriden for 4SS
        """
        self.cmd_cwd("..")
        return
        repo = self.repoFactory.getRepo()
        parcontainer = repo.fetchResource(self.cwd).getParent()
        if parcontainer:
            pardir = parcontainer.getUri()
        else:
            pardir = self.cwd
        repo.txRollback()
        self.cmd_cwd(pardir)

    def cmd_cwd(self, path):
        """
        Overriden for 4Suite Server
        """
        if path:
            repo = self._getRepo()
            try:         
                curPath = self.cwd
                if curPath[-1] != '/':
                    curPath = curPath + '/'
                newPath = FtssInputSource.FTSS_RESOLVER.normalize(path, curPath)
                print "curr path: %s, rel path: %s , new path: %s"%(curPath,path,newPath)
                base = repo.fetchResource(self.cwd)
                next = base.fetchResource(newPath)
                if not next.isResourceType(ResourceTypes.ResourceType.CONTAINER):
                    self.log_error(550,"the specified path is not a directory/container")
                    self.reply("550 the specified path is not a directory/container")
                else:
                    self.cwd = next.getAbsolutePath()                    
                    self.reply("250 Ok, going there")
            finally:
                repo.txRollback()
        

    def cmd_site(self, command):
        """
        Overiden to permit 4SS specific commands
        """
        #FIXME implement SITE command
        self.log_error(502,"This command is not yet implemented")
        self.reply("502 This command is not yet implemented")
        return

        c = string.split(command)
        cmd, arg =  string.lower(c[0]), c[1:]
        if cmd == "ps":
            sl = []
            for i in sessions.keys():
                cs = sessions[i]
                sl.append(" %i %s[%s]@%s %% %s" % (i, cs.user, cs.group, cs.ip, cs.curcmd))
            sl.append("TOTAL %i" % len(sessions))
            self.replymessage(250,sl)
            return
        elif cmd == "shutdown": # does not work
            self.reply("250 Oh dear, shutting down server")
            sys.exit(0)
        elif cmd == "kill":
            try:
                pid2kill = int(arg[0])
            except:
                self.reply("400 PID error")
                return
            if sessions.has_key(pid2kill):
                sessions[pid2kill].exit_immediately=1
            else:
                self.reply("400 No such PID")
                return
        else:
            self.reply("500 Unknown SITE command")
            return
        self.reply("250 Ok, done.")

    def cmd_noop(self,_):
        self.reply("200 NOOPing")

    def cmd_pasv(self, _):
        FTP_DATA_BOTTOM = 40000
        FTP_DATA_TOP    = 44999

        for port in xrange(FTP_DATA_BOTTOM, FTP_DATA_TOP):
            try:
                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.sock.bind(('', port))
                break
            except socket.error:
                pass
        else:            
            self.log_error(425,"Cannot be passive")
            self.reply("425 Cannot be passive")
            return
        
        self.sock.listen(1)
        adr, self.dataport = self.sock.getsockname()
        if not adr or adr == '0.0.0.0':
            adr = socket.gethostname()
        adr = socket.gethostbyname(socket.gethostname())
        if adr == '127.0.0.1':
            self.log_ftp_message("Using ServerName to determine ip for passive mode: %s"%(adr))
            addr=socket.gethostbyname(self.serverAddy)      
        
        
        adr = string.replace(adr, ".", ",")
        porthi, portlo = self.dataport/256, self.dataport%256
        self.passive = 1
        self.create_datasock = self.create_pasv_datasock
        self.log_ftp_message("227 Entering Passive Mode (%s,%i,%i)" % (adr, porthi, portlo))
        self.reply("227 Entering Passive Mode (%s,%i,%i)" % (adr, porthi, portlo))

    def cmd_port(self, port_id):
        numstr = filter(lambda x: x in "0123456789,", port_id)
        parts = string.split(numstr,",")

        try:
            hi = int(parts[-2])
            lo = int(parts[-1])
            for v in [hi,lo]:
                if v < 0 or v > 255:
                    raise ValueError
        except IndexError:
            self.log_error(501,"Are you a hacker?")
            self.reply("501 are you a hacker?")
            return
        except ValueError:
            self.log_error(501,"Uninterpretable value")
            self.reply("501 looks like nonsense to me...")
            return

        self.dataport = (hi << 8) + lo
        self.reply("230 Port is " + str(self.dataport)+ " (am ignoring specified IP for security)")

    def stor_or_appe(self, filename, comm):
        """
        Overriden for 4SS
        """
        path = self.absolutePath(filename)        
        tail = os.path.split(path)[1]
        ext = os.path.splitext(tail)[1]        

        try:
            (dd,rt,imt) = self.extendedMapping[ext]
        except KeyError:
            (dd,rt,imt) = None,None,None
                
        #self.log_ftp_message("imt is: %s"%(imt))
            
        repo = self._getRepo()
        r = "226 Phew, upload successfully completed"

        if not self.create_datasock():
            repo.txRollback()
            return

        self.abort_received = 0
        self.pendingconn = 1
        stringBuffer=StringIO()

        if repo.hasResource(path):
            resource=repo.fetchResource(path)
            if comm=='appe':
                #Appending to existing resource, append buffer w/ current
                #content
                stringBuffer.write(resource.getContent())
                stringBuffer.seek(self.restpos)

        bufferSuccessful=1
        try:
            while 1:
                if self.abort_received:
                    r = "426 Aborted"
                    self.log_error(426,"Resource transfer aborted")

                    bufferSuccessful=0
                    break

                if self.limit_stor_speed:
                    timer = time.time()
                    s = self.sock.recv(rbufsize)
                    if len(s) == 0:
                        break
                    dur = time.time()-timer
                    #speed = len(s)/dur
                    #if speed>self.limit_stor_speed:
                    if dur*self.limit_stor_speed<len(s): # to avoid division
                        time.sleep(len(s)/self.limit_stor_speed-dur)
                else:
                    s = self.sock.recv(rbufsize)
                    if len(s) == 0:
                        break
                stringBuffer.write(s)

        except socket.error:
            bufferSuccessful=0
            r = "425 Socket error"
            self.log_error(425,"Socket error")
        except IOError:
            bufferSuccessful=0
            r = "553 Upload error"
            self.log_error(553,"Upload error")
        except timeout_socket.Timeout:
            bufferSuccessful=0
            r = "425 Timeout while uploading"
            self.log_error(425,"Timeout while uploading")            
        
        try:
            self.close_datasock()
        except socket.error:
            bufferSuccessful=0
            r = "425 Socket error"
            self.log_error(425,"Socket error")

        if bufferSuccessful:
            #write buffer back to 4SS resource
            if repo.hasResource(path):
                try:
                    resource.setContent(stringBuffer.getvalue())
                except:
                    raise
                    r="553 failed to store or append to resource"
                    self.log_error(553,"Failed to store or append to resource")            
            else:
                #new resource.  Determine imt from content and extension                
                #src=
                self.log_ftp_message("NOTE: utf-8 encoding is assumed!")

                self.log_ftp_message("Storing %s with imt: %s, rt %s, dd %s"%(path,imt,rt,dd))            
                if rt is None:
                    if imt == 'text/xml' or dd is not None:
                        rt = ResourceTypes.ResourceType.XML_DOCUMENT
                    else:
                        rt = ResourceTypes.ResourceType.RAW_FILE
                if imt is None:
                    if rt == ResourceTypes.ResourceType.RAW_FILE:
                        imt = 'text/plain'
                    else:
                        imt = 'text/xml'
                
                if rt == ResourceTypes.ResourceType.RAW_FILE:
                    repo.createRawFile(path,imt,stringBuffer.getvalue())
                else:
                    repo.createDocument(path,stringBuffer.getvalue(),forcedType=rt,docDef=dd)
            repo.txCommit()
        else:
            repo.txRollback()

        stringBuffer.close()

        try:
            self.reply(r)
        except:
            pass
        self.pendingconn = 0        

    def cmd_stor(self, filename):
        if self.threading:
            thread.start_new_thread(self.stor_or_appe, (filename, "stor"))
        else:
            self.stor_or_appe(filename, "stor")

    def cmd_appe(self, filename):
        if self.threading:
            thread.start_new_thread(self.stor_or_appe, (filename, "appe"))
        else:
            self.stor_or_appe(filename, "appe")

    def cmd_dele(self, filename):
        """
        Overriden for 4SS
        """
        path=self.absolutePath(filename)
        repo = self._getRepo()
        if not repo.hasResource(path):
            self.log_error(550,"Server ran into problems deleting %s"%path)
            self.reply("550 Server ran into problems deleting %s"%path)
            repo.txRollback()
            return
        elif ResourceTypes._IsResourceType(repo.fetchResource(path).getResourceType(),ResourceTypes.ResourceType.CONTAINER):
            repo.txRollback()
            self.log_error(550,"DEL command called on a container/directory")
            self.reply("550 DEL command called on a container/directory")
            return


        try:
            repo.deleteResource(path)
            repo.txCommit()
            self.reply("250 File eliminated")
        except:
            self.log_error(550,"Unable to delete")
            self.reply("550 unable to delete")
            repo.txRollback()

    def cmd_mkd(self, dirname):
        """
        Overidden for 4SS
        """
        repo = repo = self._getRepo()
        cont = repo.fetchResource(self.cwd)

        try:
            cont.createContainer(dirname,1)
        except:
            self.log_error(550,"The server was unable to create the directory %s"%dirname)
            self.reply("550 The server was unable to create the directory %s"%dirname)
        else:
            self.reply('257 "%s" Directory created' % dirname)
        repo.txCommit()

    def cmd_rmd(self, dirname):
        """
        Overidden for 4SS
        """
        path=self.absolutePath(dirname)
        repo = self._getRepo()
        if not repo.hasResource(path):
            self.log_error(550,"Directory '%s' does not exist"%path)
            self.reply("550 Directory '%s' does not exist"%path)
            repo.txRollback()
            return
        elif not ResourceTypes._IsResourceType(repo.fetchResource(path).getResourceType(),ResourceTypes.ResourceType.CONTAINER):
            self.log_error(550,"%s is not a directory"%(path))
            self.reply("550 %s is not a directory"%(path))
            repo.txRollback()
            return
        else:            
            try:
                repo.deleteResource(path)
            except:
                repo.txRollback()
                self.log_error(550,"Server was unable to remove the directory")
                self.reply("550 Server was unable to remove the directory")
                return
            else:
                self.reply("250 Directory removed")
                repo.txCommit()
        return

    def cmd_rest(self, pos):
        try:
            self.restpos = long(pos)
            self.reply("350 Restarting. Are you happy?")
            return
        except ValueError:
            self.log_error(530,"Invalid rest value")
            self.reply("530 Sorry.")

    def cmd_retr(self, filename):
        if self.threading:
            thread.start_new_thread(self.cmd_retr1, (filename,))
        else:
            self.cmd_retr1(filename)

    def cmd_retr1(self, path):
        """
        Overriden for 4SS
        """
        if not path:
            self.log_error(550,"Resource path doesnt exist")
            self.reply("550 resource path doesnt exist")
            repo.txRollback()
            return

        repo = self._getRepo()

        stringBuffer=StringIO()
        try:
            base = repo.fetchResource(self.cwd)
            cur = base.fetchResource(path)
            stringBuffer.write(cur.getContent())
            stringBuffer.seek(0)
            if self.restpos:
                stringBuffer.seek(self.restpos)
        finally:
            repo.txRollback()

        self.restpos = 0
        r = "226 Enjoy the resource"

        try:
            if not self.create_datasock():
                return
            self.abort_received = 0
            self.pendingconn = 1
            try:
                while 1:
                    if self.abort_received:
                        r = "426 Aborted"
                        self.log_error(426,"Resource retrieval aborted")
                        break
                    s = stringBuffer.read(sbufsize)
                    if not s:
                        break
                    if self.limit_retr_speed:
                        timer = time.time()
                        self.sock.send(s)
                        dur = time.time()-timer
                        #speed = len(s)/dur
                        #if speed>self.limit_retr_speed:
                        if dur*self.limit_retr_speed<len(s): # to avoid division
                            time.sleep(len(s)/self.limit_retr_speed-dur)

                    self.sock.send(s)
            finally:
                stringBuffer.close()
                self.close_datasock()
        except socket.error:
            r = "425 Socket error"
            self.log_error(425,"Socket error")
        except (IOError, OSError):
            r = "421 File read error"
            self.log_error(421,"File read error")
        except timeout_socket.Timeout:
            r = "425 Timeout while RETRieving"
            self.log_error(425,"Timeout while RETRieving")
        try:
            self.reply(r)
        except:
            pass
        self.pendingconn = 0


    def cmd_size(self, path):
        """
        Overriden for 4SS
        """
        repo = self._getRepo()
        path = os.path.join(self.cwd, path)
        if not path or not repo.hasResource(path):
            repo.txRollback()
            self.log_error(553,"Resource path does not exist")
            self.reply("553 File read failed!")
            return
            
        res=repo.fetchResource(path)

        #ensure filename is not a container
        if ResourceTypes._IsResourceType(res.getResourceType(),ResourceTypes.ResourceType.CONTAINER):
            repo.txRollback()
            self.log_error(550,"Resource path refers to a container/directory")
            self.reply("550 resource path refers to a container/directory")            
            return

        try:
            size = repo.fetchResource(path).getSize()
        except:
            self.reply("553 resource sizing failed!")
            self.log_error(553,"Resource sizing failed")
            repo.txRollback()
            return
        self.reply("213 "+`size`)
        repo.txRollback()
        return

    def cmd_list(self, path):
        """
        Overidden for 4SS
        """
        #Account for some clients that prepend options to the specified path (Emacs - for instance)
        path = path and path.split()[-1] or path
        #Account for redundant /. appended to pathname by certain clients    (Emacs - for instance)
        path = path[-2:] == '/.' and path[:-1] or path
        if not path or path[0] == '-':
            path = self.cwd
        else:
            path = os.path.join(self.cwd, path)

        repo = self._getRepo()        
        paths = []
        try:
            if not self.create_datasock():
                return            
            
            if repo.hasResource(path):
                res = repo.fetchResource(path)
                resType = res.getResourceType()
                if res.resourceType == ResourceTypes.ResourceType.CONTAINER:
                    paths = self.unixDirListing(res)
                    #for child in res:
                    #    name=child.getName()
                    #    if ResourceTypes._IsResourceType(child.getResourceType(),ResourceTypes.ResourceType.CONTAINER):
                    #        name=name+'/'
                    #    paths.append(name)                                    
                else:
                    paths = [path]
            else:
                self.log_error(550,"Unable to locate %s in the repository"%(path))
                return

            try:            
                r = map(lambda x: "%s\r\n" % x, paths)
                try:
                    for i in r:
                        self.sock.send(i)
                except socket.error:
                    pass
                try:
                    self.close_datasock()
                except socket.error:
                    pass

                del r # to save memory
                self.reply("226 Wow, listing done")
                return
        
            finally:
                try:
                    self.close_datasock()
                except socket.error:
                    pass
            
            self.reply("226 Listing completed")
        finally:
            repo.txRollback()
        return

    # parses the command and eventually calls the appropriate routine
    def docmd(self, cmd):
        # if the connection has broken... we have to shut down:
        if cmd == "":
            raise "session_exit"
        # filter suspicious chars first
        cmd2 = filter(lambda x: ord(x) >= 32 and x not in "\377\364\362",cmd)
        lcmd2 = string.split(cmd2, None, 1) or [""]
        command = string.lower(lcmd2[0])
        if len(lcmd2) > 1:
            args = string.strip(lcmd2[1])
        else:
            args = "" # was None, but it was breaking some commands
            
        print "recieved command %s, with arguments: %s"%(command,args)

        u = self.user or "-"
        ip = self.ip or "-"
        if command == "pass":
            c = "PASS ****"
        else:
            c = cmd2

        if self.cmddict.has_key(command):
            self.log_ftp_command(command,args)            
            self.curcmd = c
            if self.threading:
                while self.pendingconn and command<>"abor":
                    pass
            try:
                try:
                    self.cmddict[command](args)
                except FtServerServerException, e:
                    if e.errorCode == Error.PERMISSION_DENIED:
                        self.log_error(550,"the specified path %s does not exist: "+e.args['path'])
                        self.reply("550 the specified path does %s not exist",e.args['path'])
                    else:
                        raise e
            except:                
                import cStringIO, traceback
                st = cStringIO.StringIO()
                traceback.print_exc(file=st)
                self.log_error(550,"Unable to complete command: %s\nException Traceback:\n%s"%(command,st.getvalue()))
                self.reply("550 I cannot:")
            return
        else:
            self.reply("500 I'm gonna ignore this command '%s'... maybe later..." % command)
            return

    def loop(self):
        while not self.exit_immediately:
            try:
                self.last_cmd_time = time.time()
                l = self.rfile.readline()
            except timeout_socket.Timeout:
                if self.pendingconn:
                    # if there is an ongoing connection, do not timeout for commands
                    # there is a subtle race here - if data connection ends while readline()
                    # is near timeout, client will get 421 Timeout as soon as the data
                    # connections finishes, not having a chance to enter control
                    # commands. But there is not much to be done about it.
                    continue # continue because we do not want to do docmd(), when
                             # readline() timeouted, l is an old value
                else:
                    try:
                        self.reply("421 timeout")
                        break
                    except (socket.error, timeout_socket.Timeout):
                        break
            try:
                self.docmd(l)
            except ("session_exit", socket.error):
                break


    def loop1(self):
        while not self.exit_immediately:
            try:
                self.last_cmd_time = time.time()
                self.docmd(self.rfile.readline())
            except "session_exit":
                break



    ################
    #Logging Method
    ################

    def log_error(self, code, msg):
        self.logFile.error("%s[FTP ERROR %s]: %s\n"%(self.log_date_time_string(),code,msg))

    def log_serverResponse(self,msg):
        self.logFile.debug("%s Server Response: %s\n"%(self.log_date_time_string(),msg))

    def log_ftp_command(self,cmd, args):
        self.logFile.info("%s FTP server recieved command : %s %s\n"%(self.log_date_time_string(),string.upper(cmd),args))
        
    def log_ftp_message(self,msg):
        self.logFile.info("%s FTP server message: %s\n"%(self.log_date_time_string(),msg))
        
    def log_date_time_string(self):
        """
        Create a time string suitable for Common Log Format.
        [dd/mmm/yyyy:HH:MM:SS ZZZZZ]
            dd - Day of the month as a decimal number [01,31]
           mmm - Abbreviated month name
          yyyy - Year with century as a decimal number
            HH - Hour (24-hour clock) as a decimal number [00,23]
            MM - Minute as a decimal number [00,59]
            SS - Second as a decimal number [00,61]
         ZZZZZ - The time-zone as hour offset from GMT in (+/-)HHMM format
        """
        localtime = time.localtime(time.time())
        # too bad that %z doesn't work in strftime()
        strtime = time.strftime('[%d/%b/%Y:%T %%+03d%%02d]', localtime)
        tzoffset = time.daylight and time.altzone or time.timezone
        (hour, min) = divmod(tzoffset, 3600)
        return strtime % (-hour, min / 3600)    



    #######################
    # Helper
    #########
    def _getRepo(self):
        return SCore.GetRepository(self._userName,
                                   self._password,
                                   self.logFile,
                                   self.properties)
        



##                        appendix = ""
##                        if child.resourceType == ResourceTypes.ResourceType.ALIAS:
##                            appendix = ' -> ' + child.referenceUri
##                            dirflag='l'
##                            desc = name + ' -> ' + child.referenceUri
##                            size = '-'

##                        elif child.isResourceType(ResourceTypes.ResourceType.FILE):
##                            dirflag='-'
##                            #size = '%.1fk' % (child.size / 1000.0)
##                            size = "%10i"%child.size
##                            desc = child.imt
##                        else:
##                            dirflag='d'
##                            if name == os.path.basename(pathInfo.uri):
##                                if pathInfo.uri == '/':
##                                    if prevName=='.':
##                                        name = '..'
##                                    else:
##                                        name = '.'
##                                else:
##                                    name = '.'

##                            elif name == os.path.basename(repo.fetchContainer(path).fetchResourceInformation('..').uri):
##                                name = '..'

##                            size = '%10i'%(4096)
##                            desc = 'container'

                        #spacer = ' ' * (32 - len(name))
                        #r.append('%-*s %-*s %-24s %6s %s%s %s\r\n' % (
                        #    max_read,readAcl,
                        #    max_write, writeAcl,
                        #    modified, size, name, spacer, desc))


    
                                

    def unixDirListing(self,parent):
        parent_abs = parent.getAbsolutePath()
        parent_with_slash = parent_abs != '/' and parent_abs + '/' or "/"

        paths = []
        repo = self._getRepo()
        container = repo.fetchResource(parent_abs)
        for child in container:        
            size = child.getSize()
            modified = Time.FromISO8601(child.getLastModifiedDate())            
            minute=str(modified.minute())
            hour=str(modified.hour())
            if len(minute)<2:
                minute = '0%s'%(minute)
            if len(hour)<2:
                hour = '0%s'%(hour)                
            monthDay = time.strftime('%b %d',modified.asPythonTimeTuple())
            hourMinute = '%s:%s'%(hour,minute)            
            unixTimeString = '%s %s'%(monthDay,hourMinute)
            
            permBits = '-rwxrwxrwx'
            if child.isResourceType(ResourceTypes.ResourceType.CONTAINER):
                permBits = 'drwxrwxrwx'
            childEntry = '%s    0 ????  ????   %7s %s %s'%(permBits, size, unixTimeString, child.getName())
            paths.append(childEntry)
        return paths
        
    def absolutePath(self,path):
        curPath = self.cwd
        if curPath[-1] != '/':
            curPath = curPath + '/'
        return Uri.BASIC_RESOLVER.normalize(path, curPath)        
    


## ls, cd, get, user, password
