Ticket #7037: 7037-2.diff

File 7037-2.diff, 24.4 KB (added by Adi Roiban, 3 years ago)
  • twisted/conch/scripts/cftp.py

    diff --git twisted/conch/scripts/cftp.py twisted/conch/scripts/cftp.py
    index 702a3f8..fc02ae0 100644
    class StdioClient(basic.LineReceiver): 
    400400            self.transport.write('\n')
    401401        return "Transferred %s to %s" % (rf.name, lf.name)
    402402
     403
    403404    def cmd_PUT(self, rest):
     405        """
     406        Do an upload request for a single local file or a globing expression.
     407
     408        @param rest: Requested command line for the PUT command.
     409        @type  rest: C{str}
     410
     411        @return: A deferred which fires when transfer is done.
     412        """
    404413        local, rest = self._getFilename(rest)
    405         if '*' in local or '?' in local: # wildcard
     414
     415        # FIXME:7241:
     416        # Use a better check for globbing expression.
     417        if '*' in local or '?' in local:
    406418            if rest:
    407419                remote, rest = self._getFilename(rest)
    408                 path = os.path.join(self.currentDirectory, remote)
    409                 d = self.client.getAttrs(path)
    410                 d.addCallback(self._cbPutTargetAttrs, remote, local)
    411                 return d
     420                remote = os.path.join(self.currentDirectory, remote)
    412421            else:
    413422                remote = ''
    414                 files = glob.glob(local)
    415                 return self._cbPutMultipleNext(None, files, remote)
    416         if rest:
    417             remote, rest = self._getFilename(rest)
     423
     424            files = glob.glob(local)
     425            return self._putMultipleFiles(files, remote)
     426
    418427        else:
    419             remote = os.path.split(local)[1]
    420         lf = file(local, 'r')
    421         path = os.path.join(self.currentDirectory, remote)
    422         flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
    423         d = self.client.openFile(path, flags, {})
    424         d.addCallback(self._cbPutOpenFile, lf)
    425         d.addErrback(self._ebCloseLf, lf)
    426         return d
     428            if rest:
     429                remote, rest = self._getFilename(rest)
     430            else:
     431                remote = os.path.split(local)[1]
     432            return self._putSingleFile(local, remote)
    427433
    428     def _cbPutTargetAttrs(self, attrs, path, local):
    429         if not stat.S_ISDIR(attrs['permissions']):
    430             return "Wildcard put with non-directory target."
    431         # FIXME:7037:
    432         # Check what `files` variable should do here.
    433         return self._cbPutMultipleNext(None, files, path)
    434434
    435     def _cbPutMultipleNext(self, res, files, path):
    436         if isinstance(res, failure.Failure):
    437             self._printFailure(res)
    438         elif res:
    439             self.transport.write(res)
    440             if not res.endswith('\n'):
     435    def _putSingleFile(self, local, remote):
     436        """
     437        Perform an upload for a single file.
     438
     439        @param local: Path to local file.
     440        @type  local: C{str}.
     441
     442        @param remote: Remote path for the request relative to current working
     443            directory.
     444        @type  remote: C{str}
     445
     446        @return: A deferred which fires when transfer is done.
     447        """
     448        return self._cbPutMultipleNext(None, [local], remote, single=True)
     449
     450
     451    def _putMultipleFiles(self, files, remote):
     452        """
     453        Perform an upload for a list of local files.
     454
     455        @param files: List of local files.
     456        @type  files: C{list} of C{str}.
     457
     458        @param remote: Remote path for the request relative to current working
     459            directory.
     460        @type  remote: C{str}
     461
     462        @return: A deferred which fires when transfer is done.
     463        """
     464        return self._cbPutMultipleNext(None, files, remote)
     465
     466
     467    def _cbPutMultipleNext(
     468            self, previousResult, files, remotePath, single=False):
     469        """
     470        Perform an upload for the next file in the list of local files.
     471
     472        @param previousResult: Result form previous file form the list.
     473        @type  previousResult: C{str}
     474
     475        @param files: List of local files.
     476        @type  files: C{list} of C{str}
     477
     478        @param remotePath: Remote path for the request relative to current
     479            working directory.
     480        @type  remotePath: C{str}
     481
     482        @param single: A flag which signals if this is a transfer for a single
     483            file in which case we use the exact remote path
     484        @type  single: C{bool}
     485
     486        @return: A deferred which fires when transfer is done.
     487        """
     488        if isinstance(previousResult, failure.Failure):
     489            self._printFailure(previousResult)
     490        elif previousResult:
     491            self.transport.write(previousResult)
     492            if not previousResult.endswith('\n'):
    441493                self.transport.write('\n')
    442         f = None
    443         while files and not f:
     494
     495        currentFile = None
     496        while files and not currentFile:
    444497            try:
    445                 f = files.pop(0)
    446                 lf = file(f, 'r')
     498                currentFile = files.pop(0)
     499                localStream = open(currentFile, 'r')
    447500            except:
    448501                self._printFailure(failure.Failure())
    449                 f = None
    450         if not f:
    451             return
    452         name = os.path.split(f)[1]
    453         remote = os.path.join(self.currentDirectory, path, name)
    454         log.msg((name, remote, path))
    455         flags = filetransfer.FXF_WRITE|filetransfer.FXF_CREAT|filetransfer.FXF_TRUNC
     502                currentFile = None
     503
     504        # No more files to transfer.
     505        if not currentFile:
     506            return None
     507
     508        if single:
     509            remote = remotePath
     510        else:
     511            name = os.path.split(currentFile)[1]
     512            remote = os.path.join(remotePath, name)
     513            log.msg((name, remote, remotePath))
     514
     515        d = self._putRemoteFile(localStream, remote)
     516        d.addBoth(self._cbPutMultipleNext, files, remotePath)
     517        return d
     518
     519
     520    def _putRemoteFile(self, localStream, remotePath):
     521        """
     522        Do an upload request.
     523
     524        @param localStream: Local stream from where data is read.
     525        @type  localStream: File like object.
     526
     527        @param remotePath: Remote path for the request relative to current
     528            working directory.
     529        @type  remotePath: C{str}
     530
     531        @return: A deferred which fires when transfer is done.
     532        """
     533        remote = os.path.join(self.currentDirectory, remotePath)
     534        flags = (
     535            filetransfer.FXF_WRITE |
     536            filetransfer.FXF_CREAT |
     537            filetransfer.FXF_TRUNC
     538            )
    456539        d = self.client.openFile(remote, flags, {})
    457         d.addCallback(self._cbPutOpenFile, lf)
    458         d.addErrback(self._ebCloseLf, lf)
    459         d.addBoth(self._cbPutMultipleNext, files, path)
     540        d.addCallback(self._cbPutOpenFile, localStream)
     541        d.addErrback(self._ebCloseLf, localStream)
    460542        return d
    461543
     544
    462545    def _cbPutOpenFile(self, rf, lf):
    463546        numRequests = self.client.transport.conn.options['requests']
    464547        if self.useProgressBar:
    class StdioClient(basic.LineReceiver): 
    528611            fullPath = self.currentDirectory + '/'
    529612        else:
    530613            fullPath = os.path.join(self.currentDirectory, path)
    531         d = self._remoteGlob(fullPath)
     614        d = selsf._remoteGlob(fullPath)
    532615        d.addCallback(self._cbDisplayFiles, options)
    533616        return d
    534617
    version Print the SFTP version. 
    736819        else:
    737820            timeLeft = 0
    738821        front = f.name
    739         back = '%3i%% %s %sps %s ' % ((total / f.size) * 100,
     822        if f.size:
     823            percentage = (total / f.size) * 100
     824        else:
     825            percentage = 100
     826        back = '%3i%% %s %sps %s ' % (percentage,
    740827                                      self._abbrevSize(total),
    741828                                      self._abbrevSize(speed),
    742829                                      self._abbrevTime(timeLeft))
    version Print the SFTP version. 
    745832
    746833
    747834    def _getFilename(self, line):
    748         line.lstrip()
     835        """
     836        Parse line received as command line input and return first filename
     837        together with the remaining line.
     838
     839        @param line: Arguments received from command line input.
     840        @type line: C{str}
     841
     842        @return: Tupple with filename and rest. Return empty values when no
     843            path was not found.
     844        @rtype: C{tupple}
     845        """
     846        line = line.strip()
    749847        if not line:
    750             return None, ''
     848            return '', ''
    751849        if line[0] in '\'"':
    752850            ret = []
    753851            line = list(line)
    version Print the SFTP version. 
    769867        if len(ret) == 1:
    770868            return ret[0], ''
    771869        else:
    772             return ret
     870            return ret[0], ret[1]
    773871
    774872StdioClient.__dict__['cmd_?'] = StdioClient.cmd_HELP
    775873
  • twisted/conch/test/test_cftp.py

    diff --git twisted/conch/test/test_cftp.py twisted/conch/test/test_cftp.py
    index 75792a4..9b5b420 100644
    import locale 
    1010import time, sys, os, operator, getpass, struct
    1111from StringIO import StringIO
    1212
     13from zope.interface import implementer
    1314from twisted.conch.test.test_ssh import Crypto, pyasn1
    1415
    1516_reason = None
    from twisted.internet import reactor, protocol, interfaces, defer, error 
    3334from twisted.internet.utils import getProcessOutputAndValue
    3435from twisted.python import log
    3536from twisted.conch import ls
     37from twisted.conch.interfaces import ISFTPFile
     38from twisted.conch.ssh import filetransfer
    3639from twisted.test.proto_helpers import StringTransport
    3740from twisted.internet.task import Clock
    3841
    class ListingTests(TestCase): 
    210213            '!---------    0 0        0               0 Sep 02 09:33 foo')
    211214
    212215
     216class InMemorySFTPClient(object):
     217    """
     218    A L{filetransfer.FileTransferClient} which does filesystem operations in
     219    memory, without touching the local disc or the network interface.
     220    """
     221
     222    def __init__(self):
     223        self.transport = StringTransport()
     224        self.transport.localClosed = False
     225        self.transport.conn = self
     226        self.openFileSideEffects = {}
     227        self.options = {
     228            'requests': 1,
     229            'buffersize': 10,
     230            }
     231
     232    def openFile(self, filename, flags, attrs):
     233        """
     234        See: L{filetransfer.FileTransferClient.openFile}.
     235
     236        Return cached file based on path and flags and remove it from cache.
     237        """
     238        return self.openFileSideEffects.pop((filename, flags))
     239
     240
     241
     242@implementer(ISFTPFile)
     243class InMemoryRemoteFile(StringIO):
     244    """
     245    An L{ISFTPFile} which handles all data in memory.
     246    """
     247
     248    def __init__(self, name):
     249        """
     250        @param name: Name of this file.
     251        @type name: C{str}
     252        """
     253        self.name = name
     254        StringIO.__init__(self)
     255
     256    def writeChunk(self, start, data):
     257        """
     258        See: L{ISFTPFile.writeChunk}
     259        """
     260        self.seek(start)
     261        self.write(data)
     262        return defer.succeed(self)
     263
     264    def close(self):
     265        """
     266        See: L{ISFTPFile.writeChunk}
     267
     268        Keeps data after file was closed to help with testing.
     269        """
     270        if not self.closed:
     271            self.closed = True
     272
     273    def getvalue(self):
     274        """
     275        Get current data of file.
     276
     277        Allow reading data event when file is closed.
     278        """
     279        if self.buflist:
     280            self.buf += ''.join(self.buflist)
     281            self.buflist = []
     282        return self.buf
     283
    213284
    214285class StdioClientTests(TestCase):
    215286    """
    class StdioClientTests(TestCase): 
    220291        Create a L{cftp.StdioClient} hooked up to dummy transport and a fake
    221292        user database.
    222293        """
    223         class Connection:
    224             pass
    225 
    226         conn = Connection()
    227         conn.transport = StringTransport()
    228         conn.transport.localClosed = False
    229 
    230         self.client = cftp.StdioClient(conn)
     294        sftpClient = InMemorySFTPClient()
     295        self.client = cftp.StdioClient(sftpClient)
     296        self.client.currentDirectory = '/'
    231297        self.database = self.client._pwd = UserDatabase()
    232298
    233299        # Intentionally bypassing makeConnection - that triggers some code
    234300        # which uses features not provided by our dumb Connection fake.
    235         self.client.transport = StringTransport()
     301        self.client.transport = self.client.client.transport
    236302
    237303
    238304    def test_exec(self):
    class StdioClientTests(TestCase): 
    294360        self.patch(cftp, "fcntl", FakeFcntl())
    295361
    296362
    297     def test_progressReporting(self):
     363    def test_printProgressBarReporting(self):
    298364        """
    299         L{StdioClient._printProgressBar} prints a progress description,
    300         including percent done, amount transferred, transfer rate, and time
    301         remaining, all based the given start time, the given L{FileWrapper}'s
    302         progress information and the reactor's current time.
     365        It prints a progress description, including percent done, amount
     366        transferred, transfer rate, and time remaining, all based the given
     367        start time, the given L{FileWrapper}'s progress information and the
     368        reactor's current time.
    303369        """
    304         # Use a short, known console width because this simple test doesn't need
    305         # to test the console padding.
     370        # Use a short, known console width because this simple test doesn't
     371        # need to test the console padding.
    306372        self.setKnownConsoleSize(10, 34)
    307373        clock = self.client.reactor = Clock()
    308374        wrapped = StringIO("x")
    class StdioClientTests(TestCase): 
    312378        startTime = clock.seconds()
    313379        clock.advance(2.0)
    314380        wrapper.total += 4096
     381
    315382        self.client._printProgressBar(wrapper, startTime)
     383
    316384        self.assertEqual(self.client.transport.value(),
    317385                          "\rsample 40% 4.0kB 2.0kBps 00:03 ")
    318386
    319387
    320     def test_reportNoProgress(self):
     388    def test_printProgressBarNoProgress(self):
    321389        """
    322         L{StdioClient._printProgressBar} prints a progress description that
    323         indicates 0 bytes transferred if no bytes have been transferred and no
     390        It prints a progress description that indicates that 0 bytes have
     391        been transferred if no bytes have been transferred and no
    324392        time has passed.
    325393        """
    326394        self.setKnownConsoleSize(10, 34)
    class StdioClientTests(TestCase): 
    329397        wrapped.name = "sample"
    330398        wrapper = cftp.FileWrapper(wrapped)
    331399        startTime = clock.seconds()
     400
    332401        self.client._printProgressBar(wrapper, startTime)
     402
    333403        self.assertEqual(self.client.transport.value(),
    334404                          "\rsample  0% 0.0B 0.0Bps 00:00 ")
    335405
    336406
     407    def test_printProgressBarEmptyFile(self):
     408        """
     409        Print the progress for empty files.
     410        """
     411        self.setKnownConsoleSize(10, 34)
     412        wrapped = StringIO()
     413        wrapped.name = 'empty-file'
     414        wrapper = cftp.FileWrapper(wrapped)
     415
     416        self.client._printProgressBar(wrapper, 0)
     417
     418        self.assertEqual(
     419            '\rempty-file100% 0.0B 0.0Bps 00:00 ',
     420            self.client.transport.value(),
     421            )
     422
     423
     424    def test_getFilenameEmpty(self):
     425        """
     426        Returns empty value for both filename and remaining data.
     427        """
     428        result = self.client._getFilename('  ')
     429
     430        self.assertEqual(('', ''), result)
     431
     432
     433    def test_getFilenameOnlyLocal(self):
     434        """
     435        Returns empty value for remaining data when line contains
     436        only a filename.
     437        """
     438        result = self.client._getFilename('only-local')
     439
     440        self.assertEqual(('only-local', ''), result)
     441
     442
     443    def test_getFilenameNotQuoted(self):
     444        """
     445        Returns filename and remaining data striped of leading and trailing
     446        spaces.
     447        """
     448        result = self.client._getFilename(' local  remote file  ')
     449
     450        self.assertEqual(('local', 'remote file'), result)
     451
     452
     453    def test_getFilenameQuoted(self):
     454        """
     455        Returns filename and remaining data not striped of leading and trailing
     456        spaces when quoted paths are requested.
     457        """
     458        result = self.client._getFilename(' " local file "  " remote  file " ')
     459
     460        self.assertEqual((' local file ', '" remote  file "'), result)
     461
     462
     463    def makeFile(self, path=None, content=b''):
     464        """
     465        Create a local file and return its path.
     466
     467        When `path` is C{None}, it will create a new temporary file.
     468
     469        @param path: Optional path for the new file.
     470        @type path: C{str}
     471
     472        @param content: Content to be written in the new file.
     473        @type content: C{bytes}
     474
     475        @return: Path to the newly create file.
     476        """
     477        if path is None:
     478            path = self.mktemp()
     479        file = open(path, 'w')
     480        file.write(content)
     481        file.close()
     482        return path
     483
     484
     485    def checkPutMessage(self, transfers,  randomOrder=False):
     486        """
     487        Check output of cftp client for a put request.
     488
     489
     490        @param transfers: List with tuple of (local, remote, progress).
     491        @param randomOrder: When set to C{True}, it will ignore the order
     492            in which put reposes are received
     493
     494        """
     495        output = self.client.transport.value().split('\n\r')
     496
     497        expectedOutput = []
     498        actualOutput = []
     499
     500        for local, remote, expected in transfers:
     501
     502            # For each transfer we have a list of reported progress which
     503            # ends with the final message informing that file was transferred.
     504            expectedTransfer = []
     505            for line in expected:
     506                expectedTransfer.append('%s%s' % (local, line))
     507            expectedTransfer.append('Transferred %s to %s' % (local, remote))
     508            expectedOutput.append(expectedTransfer)
     509
     510            progressParts = output.pop(0).strip('\r').split('\r')
     511            actual = progressParts[:-1]
     512
     513            last = progressParts[-1].strip('\n').split('\n')
     514            actual.extend(last)
     515
     516            actualTransfer = []
     517            for line in actual[:-1]:
     518                actualTransfer.append(line.rsplit(' ', 3)[0])
     519            actualTransfer.append(actual[-1])
     520            actualOutput.append(actualTransfer)
     521
     522        if randomOrder:
     523            self.assertItemsEqual(expectedOutput, actualOutput)
     524        else:
     525            self.assertEqual(expectedOutput, actualOutput)
     526
     527        self.assertEqual(
     528            0, len(output),
     529            'There are still put responses which were not checked.',
     530            )
     531
     532    def test_cmd_PUTSingleNoRemotePath(self):
     533        """
     534        A name based on local path is used when remote path is not
     535        provided.
     536
     537        The progress is updated while chunks are transferred.
     538        """
     539        content = 'Test\r\nContent'
     540        localPath = self.makeFile(content=content)
     541        flags = (
     542            filetransfer.FXF_WRITE |
     543            filetransfer.FXF_CREAT |
     544            filetransfer.FXF_TRUNC
     545            )
     546        remoteName = os.path.join('/', os.path.basename(localPath))
     547        remoteFile = InMemoryRemoteFile(remoteName)
     548        self.client.client.openFileSideEffects[(remoteName, flags)] = (
     549            defer.succeed(remoteFile))
     550        self.client.client.options['buffersize'] = 10
     551
     552        deferred = self.client.cmd_PUT(localPath)
     553        self.successResultOf(deferred)
     554
     555        self.assertEqual(content, remoteFile.getvalue())
     556        self.assertTrue(remoteFile.closed)
     557        self.checkPutMessage(
     558            [(localPath, remoteName,
     559                [' 76% 10.0B', '100% 13.0B', '100% 13.0B'])])
     560
     561
     562    def test_cmd_PUTSingleRemotePath(self):
     563        """
     564        Remote path is extracted from first filename after local file.
     565
     566        Any other data in the line is ignored.
     567        """
     568        localPath = self.makeFile()
     569        flags = (
     570            filetransfer.FXF_WRITE |
     571            filetransfer.FXF_CREAT |
     572            filetransfer.FXF_TRUNC
     573            )
     574        remoteName = '/remote-path'
     575        remoteFile = InMemoryRemoteFile(remoteName)
     576        self.client.client.openFileSideEffects[(remoteName, flags)] = (
     577            defer.succeed(remoteFile))
     578
     579        deferred = self.client.cmd_PUT(
     580            '%s %s ignored' % (localPath, remoteName))
     581        self.successResultOf(deferred)
     582
     583        self.checkPutMessage([(localPath, remoteName, ['100% 0.0B'])])
     584        self.assertTrue(remoteFile.closed)
     585        self.assertEqual('', remoteFile.getvalue())
     586
     587
     588    def test_cmd_PUTMultipleNoRemotePath(self):
     589        """
     590        When a gobbing expression is used local files are transfered with
     591        remote file names based on local names.
     592        """
     593        first = self.makeFile()
     594        firstName = os.path.basename(first)
     595        secondName = 'second-name'
     596        parent = os.path.dirname(first)
     597        second = self.makeFile(path=os.path.join(parent, secondName))
     598        flags = (
     599            filetransfer.FXF_WRITE |
     600            filetransfer.FXF_CREAT |
     601            filetransfer.FXF_TRUNC
     602            )
     603        firstRemotePath = '/%s' % (firstName,)
     604        secondRemotePath = '/%s' % (secondName,)
     605        firstRemoteFile = InMemoryRemoteFile(firstRemotePath)
     606        secondRemoteFile = InMemoryRemoteFile(secondRemotePath)
     607        self.client.client.openFileSideEffects[(firstRemotePath, flags)] = (
     608            defer.succeed(firstRemoteFile))
     609        self.client.client.openFileSideEffects[(secondRemotePath, flags)] = (
     610            defer.succeed(secondRemoteFile))
     611
     612        deferred = self.client.cmd_PUT(os.path.join(parent, '*'))
     613        self.successResultOf(deferred)
     614
     615        self.assertTrue(firstRemoteFile.closed)
     616        self.assertEqual('', firstRemoteFile.getvalue())
     617        self.assertTrue(secondRemoteFile.closed)
     618        self.assertEqual('', secondRemoteFile.getvalue())
     619        self.checkPutMessage([
     620            (first, firstRemotePath, ['100% 0.0B']),
     621            (second, secondRemotePath, ['100% 0.0B']),
     622            ],
     623            randomOrder=True,
     624            )
     625
     626
     627    def test_cmd_PUTMultipleWithRemotePath(self):
     628        """
     629        When a gobbing expression is used local files are transfered with
     630        remote file names based on local names.
     631        when a remote folder is requested remote paths are composed from
     632        remote path and local filename.
     633        """
     634        first = self.makeFile()
     635        firstName = os.path.basename(first)
     636        secondName = 'second-name'
     637        parent = os.path.dirname(first)
     638        second = self.makeFile(path=os.path.join(parent, secondName))
     639        flags = (
     640            filetransfer.FXF_WRITE |
     641            filetransfer.FXF_CREAT |
     642            filetransfer.FXF_TRUNC
     643            )
     644        firstRemoteFile = InMemoryRemoteFile(firstName)
     645        secondRemoteFile = InMemoryRemoteFile(secondName)
     646        firstRemotePath = '/remote/%s' % (firstName,)
     647        secondRemotePath = '/remote/%s' % (secondName,)
     648        self.client.client.openFileSideEffects[(firstRemotePath, flags)] = (
     649            defer.succeed(firstRemoteFile))
     650        self.client.client.openFileSideEffects[(secondRemotePath, flags)] = (
     651            defer.succeed(secondRemoteFile))
     652
     653        deferred = self.client.cmd_PUT('%s remote' % (os.path.join(parent, '*')))
     654        self.successResultOf(deferred)
     655
     656        self.assertTrue(firstRemoteFile.closed)
     657        self.assertEqual('', firstRemoteFile.getvalue())
     658        self.assertTrue(secondRemoteFile.closed)
     659        self.assertEqual('', secondRemoteFile.getvalue())
     660        self.checkPutMessage([
     661            (first, firstName, ['100% 0.0B']),
     662            (second, secondName, ['100% 0.0B']),
     663            ],
     664            randomOrder=True,
     665            )
     666
    337667
    338668class FileTransferTestRealm:
    339669    def __init__(self, testDir):
    class CFTPClientTestBase(SFTPTestBase): 
    510840
    511841
    512842class TestOurServerCmdLineClient(CFTPClientTestBase):
     843    """
     844    Functional tests which launch a SFTP server over TCP on localhost and
     845    check cftp command line interface using a spawned process.
     846
     847        Due to the spawned process you can not add a debugger breakpoint for the
     848    client code.
     849    """
    513850
    514851    def setUp(self):
    515852        CFTPClientTestBase.setUp(self)
    class TestOurServerCmdLineClient(CFTPClientTestBase): 
    8421179
    8431180
    8441181class TestOurServerBatchFile(CFTPClientTestBase):
     1182    """
     1183    Functional tests which launch a SFTP server over localhost and
     1184    checks csftp in batch interface.
     1185    """
     1186
    8451187    def setUp(self):
    8461188        CFTPClientTestBase.setUp(self)
    8471189        self.startServer()