~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Vincent Ladeuil
  • Date: 2007-06-20 14:25:06 UTC
  • mfrom: (2540 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2646.
  • Revision ID: v.ladeuil+lp@free.fr-20070620142506-txsb1v8538kpsafw
merge bzr.dev @ 2540

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
"""Implementation of Transport over ftp.
17
17
 
18
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
 
28
import asyncore
 
29
import errno
28
30
import ftplib
29
 
import getpass
30
31
import os
31
 
import random
32
 
import socket
 
32
import os.path
 
33
import urllib
 
34
import urlparse
 
35
import select
33
36
import stat
 
37
import threading
34
38
import time
 
39
import random
 
40
from warnings import warn
35
41
 
36
42
from bzrlib import (
37
 
    config,
38
43
    errors,
39
44
    osutils,
40
45
    urlutils,
41
46
    )
42
47
from bzrlib.trace import mutter, warning
43
48
from bzrlib.transport import (
44
 
    AppendBasedFileStream,
 
49
    Server,
45
50
    ConnectedTransport,
46
 
    _file_streams,
47
 
    register_urlparse_netloc_protocol,
48
 
    Server,
49
51
    )
50
 
 
51
 
 
52
 
register_urlparse_netloc_protocol('aftp')
 
52
from bzrlib.transport.local import LocalURLServer
 
53
import bzrlib.ui
 
54
 
 
55
_have_medusa = False
53
56
 
54
57
 
55
58
class FtpPathError(errors.PathError):
57
60
 
58
61
 
59
62
class FtpStatResult(object):
60
 
 
61
 
    def __init__(self, f, abspath):
 
63
    def __init__(self, f, relpath):
62
64
        try:
63
 
            self.st_size = f.size(abspath)
 
65
            self.st_size = f.size(relpath)
64
66
            self.st_mode = stat.S_IFREG
65
67
        except ftplib.error_perm:
66
68
            pwd = f.pwd()
67
69
            try:
68
 
                f.cwd(abspath)
 
70
                f.cwd(relpath)
69
71
                self.st_mode = stat.S_IFDIR
70
72
            finally:
71
73
                f.cwd(pwd)
77
79
# FIXME: there are inconsistencies in the way temporary errors are
78
80
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
79
81
# be taken to analyze the implications for write operations (read operations
80
 
# are safe to retry). Overall even some read operations are never
81
 
# retried. --vila 20070720 (Bug #127164)
 
82
# are safe to retry). Overall even some read operations are never retried.
82
83
class FtpTransport(ConnectedTransport):
83
84
    """This is the transport agent for ftp:// access."""
84
85
 
85
 
    def __init__(self, base, _from_transport=None):
 
86
    def __init__(self, base, from_transport=None):
86
87
        """Set the base path where files will be stored."""
87
 
        if not (base.startswith('ftp://') or base.startswith('aftp://')):
88
 
            raise ValueError(base)
89
 
        super(FtpTransport, self).__init__(base,
90
 
                                           _from_transport=_from_transport)
 
88
        assert base.startswith('ftp://') or base.startswith('aftp://')
 
89
        super(FtpTransport, self).__init__(base, from_transport)
91
90
        self._unqualified_scheme = 'ftp'
92
91
        if self._scheme == 'aftp':
93
92
            self.is_active = True
94
93
        else:
95
94
            self.is_active = False
96
95
 
97
 
        # Most modern FTP servers support the APPE command. If ours doesn't, we
98
 
        # (re)set this flag accordingly later.
99
 
        self._has_append = True
100
 
 
101
96
    def _get_FTP(self):
102
97
        """Return the ftplib.FTP instance for this object."""
103
98
        # Ensures that a connection is established
108
103
            self._set_connection(connection, credentials)
109
104
        return connection
110
105
 
111
 
    connection_class = ftplib.FTP
112
 
 
113
106
    def _create_connection(self, credentials=None):
114
107
        """Create a new connection with the provided credentials.
115
108
 
117
110
 
118
111
        :return: The created connection and its associated credentials.
119
112
 
120
 
        The input credentials are only the password as it may have been
121
 
        entered interactively by the user and may be different from the one
122
 
        provided in base url at transport creation time.  The returned
123
 
        credentials are username, password.
 
113
        The credentials are only the password as it may have been entered
 
114
        interactively by the user and may be different from the one provided
 
115
        in base url at transport creation time.
124
116
        """
125
117
        if credentials is None:
126
 
            user, password = self._user, self._password
 
118
            password = self._password
127
119
        else:
128
 
            user, password = credentials
 
120
            password = credentials
129
121
 
130
 
        auth = config.AuthenticationConfig()
131
 
        if user is None:
132
 
            user = auth.get_user('ftp', self._host, port=self._port,
133
 
                                 default=getpass.getuser())
134
122
        mutter("Constructing FTP instance against %r" %
135
 
               ((self._host, self._port, user, '********',
 
123
               ((self._host, self._port, self._user, '********',
136
124
                self.is_active),))
137
125
        try:
138
 
            connection = self.connection_class()
 
126
            connection = ftplib.FTP()
139
127
            connection.connect(host=self._host, port=self._port)
140
 
            self._login(connection, auth, user, password)
 
128
            if self._user and self._user != 'anonymous' and \
 
129
                    password is not None: # '' is a valid password
 
130
                get_password = bzrlib.ui.ui_factory.get_password
 
131
                password = get_password(prompt='FTP %(user)s@%(host)s password',
 
132
                                        user=self._user, host=self._host)
 
133
            connection.login(user=self._user, passwd=password)
141
134
            connection.set_pasv(not self.is_active)
142
 
            # binary mode is the default
143
 
            connection.voidcmd('TYPE I')
144
 
        except socket.error, e:
145
 
            raise errors.SocketConnectionError(self._host, self._port,
146
 
                                               msg='Unable to connect to',
147
 
                                               orig_error= e)
148
135
        except ftplib.error_perm, e:
149
136
            raise errors.TransportError(msg="Error setting up connection:"
150
137
                                        " %s" % str(e), orig_error=e)
151
 
        return connection, (user, password)
152
 
 
153
 
    def _login(self, connection, auth, user, password):
154
 
        # '' is a valid password
155
 
        if user and user != 'anonymous' and password is None:
156
 
            password = auth.get_password('ftp', self._host,
157
 
                                         user, port=self._port)
158
 
        connection.login(user=user, passwd=password)
 
138
        return connection, password
159
139
 
160
140
    def _reconnect(self):
161
141
        """Create a new connection with the previously used credentials"""
162
 
        credentials = self._get_credentials()
 
142
        credentials = self.get_credentials()
163
143
        connection, credentials = self._create_connection(credentials)
164
144
        self._set_connection(connection, credentials)
165
145
 
182
162
            or 'could not open' in s
183
163
            or 'no such dir' in s
184
164
            or 'could not create file' in s # vsftpd
185
 
            or 'file doesn\'t exist' in s
186
 
            or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
187
 
            or 'file/directory not found' in s # filezilla server
188
 
            # Microsoft FTP-Service RNFR reply if file not found
189
 
            or (s.startswith('550 ') and 'unable to rename to' in extra)
190
165
            ):
191
166
            raise errors.NoSuchFile(path, extra=extra)
192
167
        if ('file exists' in s):
198
173
 
199
174
        if unknown_exc:
200
175
            raise unknown_exc(path, extra=extra)
201
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in
 
176
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
202
177
        #       something like TransportError, but this loses the traceback
203
178
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
204
179
        #       to handle. Consider doing something like that here.
205
180
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
206
181
        raise
207
182
 
 
183
    def should_cache(self):
 
184
        """Return True if the data pulled across should be cached locally.
 
185
        """
 
186
        return True
 
187
 
 
188
    def _remote_path(self, relpath):
 
189
        # XXX: It seems that ftplib does not handle Unicode paths
 
190
        # at the same time, medusa won't handle utf8 paths So if
 
191
        # we .encode(utf8) here (see ConnectedTransport
 
192
        # implementation), then we get a Server failure.  while
 
193
        # if we use str(), we get a UnicodeError, and the test
 
194
        # suite just skips testing UnicodePaths.
 
195
        relative = str(urlutils.unescape(relpath))
 
196
        remote_path = self._combine_paths(self._path, relative)
 
197
        return remote_path
 
198
 
208
199
    def has(self, relpath):
209
200
        """Does the target location exist?"""
210
201
        # FIXME jam 20060516 We *do* ask about directories in the test suite
280
271
        abspath = self._remote_path(relpath)
281
272
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
282
273
                        os.getpid(), random.randint(0,0x7FFFFFFF))
283
 
        bytes = None
284
274
        if getattr(fp, 'read', None) is None:
285
 
            # hand in a string IO
286
 
            bytes = fp
287
 
            fp = StringIO(bytes)
288
 
        else:
289
 
            # capture the byte count; .read() may be read only so
290
 
            # decorate it.
291
 
            class byte_counter(object):
292
 
                def __init__(self, fp):
293
 
                    self.fp = fp
294
 
                    self.counted_bytes = 0
295
 
                def read(self, count):
296
 
                    result = self.fp.read(count)
297
 
                    self.counted_bytes += len(result)
298
 
                    return result
299
 
            fp = byte_counter(fp)
 
275
            fp = StringIO(fp)
300
276
        try:
301
277
            mutter("FTP put: %s", abspath)
302
278
            f = self._get_FTP()
303
279
            try:
304
280
                f.storbinary('STOR '+tmp_abspath, fp)
305
281
                self._rename_and_overwrite(tmp_abspath, abspath, f)
306
 
                self._setmode(relpath, mode)
307
 
                if bytes is not None:
308
 
                    return len(bytes)
309
 
                else:
310
 
                    return fp.counted_bytes
311
282
            except (ftplib.error_temp,EOFError), e:
312
283
                warning("Failure during ftp PUT. Deleting temporary file.")
313
284
                try:
318
289
                    raise e
319
290
                raise
320
291
        except ftplib.error_perm, e:
321
 
            self._translate_perm_error(e, abspath, extra='could not store',
322
 
                                       unknown_exc=errors.NoSuchFile)
 
292
            self._translate_perm_error(e, abspath, extra='could not store')
323
293
        except ftplib.error_temp, e:
324
294
            if retries > _number_of_retries:
325
295
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
345
315
            mutter("FTP mkd: %s", abspath)
346
316
            f = self._get_FTP()
347
317
            f.mkd(abspath)
348
 
            self._setmode(relpath, mode)
349
318
        except ftplib.error_perm, e:
350
319
            self._translate_perm_error(e, abspath,
351
320
                unknown_exc=errors.FileExists)
352
321
 
353
 
    def open_write_stream(self, relpath, mode=None):
354
 
        """See Transport.open_write_stream."""
355
 
        self.put_bytes(relpath, "", mode)
356
 
        result = AppendBasedFileStream(self, relpath)
357
 
        _file_streams[self.abspath(relpath)] = result
358
 
        return result
359
 
 
360
 
    def recommended_page_size(self):
361
 
        """See Transport.recommended_page_size().
362
 
 
363
 
        For FTP we suggest a large page size to reduce the overhead
364
 
        introduced by latency.
365
 
        """
366
 
        return 64 * 1024
367
 
 
368
322
    def rmdir(self, rel_path):
369
323
        """Delete the directory at rel_path"""
370
324
        abspath = self._remote_path(rel_path)
379
333
        """Append the text in the file-like object into the final
380
334
        location.
381
335
        """
382
 
        text = f.read()
383
336
        abspath = self._remote_path(relpath)
384
337
        if self.has(relpath):
385
338
            ftp = self._get_FTP()
387
340
        else:
388
341
            result = 0
389
342
 
390
 
        if self._has_append:
391
 
            mutter("FTP appe to %s", abspath)
392
 
            self._try_append(relpath, text, mode)
393
 
        else:
394
 
            self._fallback_append(relpath, text, mode)
 
343
        mutter("FTP appe to %s", abspath)
 
344
        self._try_append(relpath, f.read(), mode)
395
345
 
396
346
        return result
397
347
 
398
348
    def _try_append(self, relpath, text, mode=None, retries=0):
399
349
        """Try repeatedly to append the given text to the file at relpath.
400
 
 
 
350
        
401
351
        This is a recursive function. On errors, it will be called until the
402
352
        number of retries is exceeded.
403
353
        """
405
355
            abspath = self._remote_path(relpath)
406
356
            mutter("FTP appe (try %d) to %s", retries, abspath)
407
357
            ftp = self._get_FTP()
 
358
            ftp.voidcmd("TYPE I")
408
359
            cmd = "APPE %s" % abspath
409
360
            conn = ftp.transfercmd(cmd)
410
361
            conn.sendall(text)
411
362
            conn.close()
412
 
            self._setmode(relpath, mode)
 
363
            if mode:
 
364
                self._setmode(relpath, mode)
413
365
            ftp.getresp()
414
366
        except ftplib.error_perm, e:
415
 
            # Check whether the command is not supported (reply code 502)
416
 
            if str(e).startswith('502 '):
417
 
                warning("FTP server does not support file appending natively. "
418
 
                        "Performance may be severely degraded! (%s)", e)
419
 
                self._has_append = False
420
 
                self._fallback_append(relpath, text, mode)
421
 
            else:
422
 
                self._translate_perm_error(e, abspath, extra='error appending',
423
 
                    unknown_exc=errors.NoSuchFile)
 
367
            self._translate_perm_error(e, abspath, extra='error appending',
 
368
                unknown_exc=errors.NoSuchFile)
424
369
        except ftplib.error_temp, e:
425
370
            if retries > _number_of_retries:
426
 
                raise errors.TransportError(
427
 
                    "FTP temporary error during APPEND %s. Aborting."
428
 
                    % abspath, orig_error=e)
 
371
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
372
                        "Aborting." % abspath, orig_error=e)
429
373
            else:
430
374
                warning("FTP temporary error: %s. Retrying.", str(e))
431
375
                self._reconnect()
432
376
                self._try_append(relpath, text, mode, retries+1)
433
377
 
434
 
    def _fallback_append(self, relpath, text, mode = None):
435
 
        remote = self.get(relpath)
436
 
        remote.seek(0, os.SEEK_END)
437
 
        remote.write(text)
438
 
        remote.seek(0)
439
 
        return self.put_file(relpath, remote, mode)
440
 
 
441
378
    def _setmode(self, relpath, mode):
442
379
        """Set permissions on a path.
443
380
 
444
381
        Only set permissions if the FTP server supports the 'SITE CHMOD'
445
382
        extension.
446
383
        """
447
 
        if mode:
448
 
            try:
449
 
                mutter("FTP site chmod: setting permissions to %s on %s",
450
 
                       oct(mode), self._remote_path(relpath))
451
 
                ftp = self._get_FTP()
452
 
                cmd = "SITE CHMOD %s %s" % (oct(mode),
453
 
                                            self._remote_path(relpath))
454
 
                ftp.sendcmd(cmd)
455
 
            except ftplib.error_perm, e:
456
 
                # Command probably not available on this server
457
 
                warning("FTP Could not set permissions to %s on %s. %s",
458
 
                        oct(mode), self._remote_path(relpath), str(e))
 
384
        try:
 
385
            mutter("FTP site chmod: setting permissions to %s on %s",
 
386
                str(mode), self._remote_path(relpath))
 
387
            ftp = self._get_FTP()
 
388
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
 
389
            ftp.sendcmd(cmd)
 
390
        except ftplib.error_perm, e:
 
391
            # Command probably not available on this server
 
392
            warning("FTP Could not set permissions to %s on %s. %s",
 
393
                    str(mode), self._remote_path(relpath), str(e))
459
394
 
460
395
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
461
396
    #       to copy something to another machine. And you may be able
486
421
            self._rename_and_overwrite(abs_from, abs_to, f)
487
422
        except ftplib.error_perm, e:
488
423
            self._translate_perm_error(e, abs_from,
489
 
                extra='unable to rename to %r' % (rel_to,),
 
424
                extra='unable to rename to %r' % (rel_to,), 
490
425
                unknown_exc=errors.PathError)
491
426
 
492
427
    def _rename_and_overwrite(self, abs_from, abs_to, f):
512
447
            self._translate_perm_error(e, abspath, 'error deleting',
513
448
                unknown_exc=errors.NoSuchFile)
514
449
 
515
 
    def external_url(self):
516
 
        """See bzrlib.transport.Transport.external_url."""
517
 
        # FTP URL's are externally usable.
518
 
        return self.base
519
 
 
520
450
    def listable(self):
521
451
        """See Transport.listable."""
522
452
        return True
527
457
        mutter("FTP nlst: %s", basepath)
528
458
        f = self._get_FTP()
529
459
        try:
530
 
            try:
531
 
                paths = f.nlst(basepath)
532
 
            except ftplib.error_perm, e:
533
 
                self._translate_perm_error(e, relpath,
534
 
                                           extra='error with list_dir')
535
 
            except ftplib.error_temp, e:
536
 
                # xs4all's ftp server raises a 450 temp error when listing an
537
 
                # empty directory. Check for that and just return an empty list
538
 
                # in that case. See bug #215522
539
 
                if str(e).lower().startswith('450 no files found'):
540
 
                    mutter('FTP Server returned "%s" for nlst.'
541
 
                           ' Assuming it means empty directory',
542
 
                           str(e))
543
 
                    return []
544
 
                raise
545
 
        finally:
546
 
            # Restore binary mode as nlst switch to ascii mode to retrieve file
547
 
            # list
548
 
            f.voidcmd('TYPE I')
549
 
 
 
460
            paths = f.nlst(basepath)
 
461
        except ftplib.error_perm, e:
 
462
            self._translate_perm_error(e, relpath, extra='error with list_dir')
550
463
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
551
464
        if paths and paths[0].startswith(basepath):
552
465
            entries = [path[len(basepath)+1:] for path in paths]
603
516
        return self.lock_read(relpath)
604
517
 
605
518
 
 
519
class FtpServer(Server):
 
520
    """Common code for SFTP server facilities."""
 
521
 
 
522
    def __init__(self):
 
523
        self._root = None
 
524
        self._ftp_server = None
 
525
        self._port = None
 
526
        self._async_thread = None
 
527
        # ftp server logs
 
528
        self.logs = []
 
529
 
 
530
    def get_url(self):
 
531
        """Calculate an ftp url to this server."""
 
532
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
 
533
 
 
534
#    def get_bogus_url(self):
 
535
#        """Return a URL which cannot be connected to."""
 
536
#        return 'ftp://127.0.0.1:1'
 
537
 
 
538
    def log(self, message):
 
539
        """This is used by medusa.ftp_server to log connections, etc."""
 
540
        self.logs.append(message)
 
541
 
 
542
    def setUp(self, vfs_server=None):
 
543
        if not _have_medusa:
 
544
            raise RuntimeError('Must have medusa to run the FtpServer')
 
545
 
 
546
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
 
547
            "FtpServer currently assumes local transport, got %s" % vfs_server
 
548
 
 
549
        self._root = os.getcwdu()
 
550
        self._ftp_server = _ftp_server(
 
551
            authorizer=_test_authorizer(root=self._root),
 
552
            ip='localhost',
 
553
            port=0, # bind to a random port
 
554
            resolver=None,
 
555
            logger_object=self # Use FtpServer.log() for messages
 
556
            )
 
557
        self._port = self._ftp_server.getsockname()[1]
 
558
        # Don't let it loop forever, or handle an infinite number of requests.
 
559
        # In this case it will run for 1000s, or 10000 requests
 
560
        self._async_thread = threading.Thread(
 
561
                target=FtpServer._asyncore_loop_ignore_EBADF,
 
562
                kwargs={'timeout':0.1, 'count':10000})
 
563
        self._async_thread.setDaemon(True)
 
564
        self._async_thread.start()
 
565
 
 
566
    def tearDown(self):
 
567
        """See bzrlib.transport.Server.tearDown."""
 
568
        # have asyncore release the channel
 
569
        self._ftp_server.del_channel()
 
570
        asyncore.close_all()
 
571
        self._async_thread.join()
 
572
 
 
573
    @staticmethod
 
574
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
 
575
        """Ignore EBADF during server shutdown.
 
576
 
 
577
        We close the socket to get the server to shutdown, but this causes
 
578
        select.select() to raise EBADF.
 
579
        """
 
580
        try:
 
581
            asyncore.loop(*args, **kwargs)
 
582
            # FIXME: If we reach that point, we should raise an exception
 
583
            # explaining that the 'count' parameter in setUp is too low or
 
584
            # testers may wonder why their test just sits there waiting for a
 
585
            # server that is already dead. Note that if the tester waits too
 
586
            # long under pdb the server will also die.
 
587
        except select.error, e:
 
588
            if e.args[0] != errno.EBADF:
 
589
                raise
 
590
 
 
591
 
 
592
_ftp_channel = None
 
593
_ftp_server = None
 
594
_test_authorizer = None
 
595
 
 
596
 
 
597
def _setup_medusa():
 
598
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
 
599
    try:
 
600
        import medusa
 
601
        import medusa.filesys
 
602
        import medusa.ftp_server
 
603
    except ImportError:
 
604
        return False
 
605
 
 
606
    _have_medusa = True
 
607
 
 
608
    class test_authorizer(object):
 
609
        """A custom Authorizer object for running the test suite.
 
610
 
 
611
        The reason we cannot use dummy_authorizer, is because it sets the
 
612
        channel to readonly, which we don't always want to do.
 
613
        """
 
614
 
 
615
        def __init__(self, root):
 
616
            self.root = root
 
617
 
 
618
        def authorize(self, channel, username, password):
 
619
            """Return (success, reply_string, filesystem)"""
 
620
            if not _have_medusa:
 
621
                return 0, 'No Medusa.', None
 
622
 
 
623
            channel.persona = -1, -1
 
624
            if username == 'anonymous':
 
625
                channel.read_only = 1
 
626
            else:
 
627
                channel.read_only = 0
 
628
 
 
629
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
 
630
 
 
631
 
 
632
    class ftp_channel(medusa.ftp_server.ftp_channel):
 
633
        """Customized ftp channel"""
 
634
 
 
635
        def log(self, message):
 
636
            """Redirect logging requests."""
 
637
            mutter('_ftp_channel: %s', message)
 
638
 
 
639
        def log_info(self, message, type='info'):
 
640
            """Redirect logging requests."""
 
641
            mutter('_ftp_channel %s: %s', type, message)
 
642
 
 
643
        def cmd_rnfr(self, line):
 
644
            """Prepare for renaming a file."""
 
645
            self._renaming = line[1]
 
646
            self.respond('350 Ready for RNTO')
 
647
            # TODO: jam 20060516 in testing, the ftp server seems to
 
648
            #       check that the file already exists, or it sends
 
649
            #       550 RNFR command failed
 
650
 
 
651
        def cmd_rnto(self, line):
 
652
            """Rename a file based on the target given.
 
653
 
 
654
            rnto must be called after calling rnfr.
 
655
            """
 
656
            if not self._renaming:
 
657
                self.respond('503 RNFR required first.')
 
658
            pfrom = self.filesystem.translate(self._renaming)
 
659
            self._renaming = None
 
660
            pto = self.filesystem.translate(line[1])
 
661
            if os.path.exists(pto):
 
662
                self.respond('550 RNTO failed: file exists')
 
663
                return
 
664
            try:
 
665
                os.rename(pfrom, pto)
 
666
            except (IOError, OSError), e:
 
667
                # TODO: jam 20060516 return custom responses based on
 
668
                #       why the command failed
 
669
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
 
670
                # sometimes don't provide expected error message;
 
671
                # so we obtain such message via os.strerror()
 
672
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
 
673
            except:
 
674
                self.respond('550 RNTO failed')
 
675
                # For a test server, we will go ahead and just die
 
676
                raise
 
677
            else:
 
678
                self.respond('250 Rename successful.')
 
679
 
 
680
        def cmd_size(self, line):
 
681
            """Return the size of a file
 
682
 
 
683
            This is overloaded to help the test suite determine if the 
 
684
            target is a directory.
 
685
            """
 
686
            filename = line[1]
 
687
            if not self.filesystem.isfile(filename):
 
688
                if self.filesystem.isdir(filename):
 
689
                    self.respond('550 "%s" is a directory' % (filename,))
 
690
                else:
 
691
                    self.respond('550 "%s" is not a file' % (filename,))
 
692
            else:
 
693
                self.respond('213 %d' 
 
694
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
 
695
 
 
696
        def cmd_mkd(self, line):
 
697
            """Create a directory.
 
698
 
 
699
            Overloaded because default implementation does not distinguish
 
700
            *why* it cannot make a directory.
 
701
            """
 
702
            if len (line) != 2:
 
703
                self.command_not_understood(''.join(line))
 
704
            else:
 
705
                path = line[1]
 
706
                try:
 
707
                    self.filesystem.mkdir (path)
 
708
                    self.respond ('257 MKD command successful.')
 
709
                except (IOError, OSError), e:
 
710
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
 
711
                    # sometimes don't provide expected error message;
 
712
                    # so we obtain such message via os.strerror()
 
713
                    self.respond ('550 error creating directory: %s' %
 
714
                                  os.strerror(e.errno))
 
715
                except:
 
716
                    self.respond ('550 error creating directory.')
 
717
 
 
718
 
 
719
    class ftp_server(medusa.ftp_server.ftp_server):
 
720
        """Customize the behavior of the Medusa ftp_server.
 
721
 
 
722
        There are a few warts on the ftp_server, based on how it expects
 
723
        to be used.
 
724
        """
 
725
        _renaming = None
 
726
        ftp_channel_class = ftp_channel
 
727
 
 
728
        def __init__(self, *args, **kwargs):
 
729
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
 
730
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
 
731
 
 
732
        def log(self, message):
 
733
            """Redirect logging requests."""
 
734
            mutter('_ftp_server: %s', message)
 
735
 
 
736
        def log_info(self, message, type='info'):
 
737
            """Override the asyncore.log_info so we don't stipple the screen."""
 
738
            mutter('_ftp_server %s: %s', type, message)
 
739
 
 
740
    _test_authorizer = test_authorizer
 
741
    _ftp_channel = ftp_channel
 
742
    _ftp_server = ftp_server
 
743
 
 
744
    return True
 
745
 
 
746
 
606
747
def get_test_permutations():
607
748
    """Return the permutations to be used in testing."""
608
 
    from bzrlib.tests import ftp_server
609
 
    return [(FtpTransport, ftp_server.FTPTestServer)]
 
749
    if not _setup_medusa():
 
750
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
 
751
        return []
 
752
    else:
 
753
        return [(FtpTransport, FtpServer)]