~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Vincent Ladeuil
  • Date: 2008-01-03 08:49:38 UTC
  • mfrom: (3111.1.31 175524)
  • mto: This revision was merged to the branch mainline in revision 3158.
  • Revision ID: v.ladeuil+lp@free.fr-20080103084938-7kvurk5uvde2ui54
Fix bug #175524, http test servers are 1.1 compliant

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
28
 
import asyncore
29
28
import errno
30
29
import ftplib
 
30
import getpass
31
31
import os
32
32
import os.path
33
 
import urllib
34
33
import urlparse
35
 
import select
 
34
import socket
36
35
import stat
37
 
import threading
38
36
import time
39
37
import random
40
38
from warnings import warn
41
39
 
42
40
from bzrlib import (
 
41
    config,
43
42
    errors,
44
43
    osutils,
45
44
    urlutils,
46
45
    )
47
46
from bzrlib.trace import mutter, warning
48
47
from bzrlib.transport import (
 
48
    AppendBasedFileStream,
 
49
    ConnectedTransport,
 
50
    _file_streams,
 
51
    register_urlparse_netloc_protocol,
49
52
    Server,
50
 
    split_url,
51
 
    Transport,
52
53
    )
53
54
from bzrlib.transport.local import LocalURLServer
54
55
import bzrlib.ui
55
56
 
56
 
_have_medusa = False
 
57
 
 
58
register_urlparse_netloc_protocol('aftp')
57
59
 
58
60
 
59
61
class FtpPathError(errors.PathError):
60
62
    """FTP failed for path: %(path)s%(extra)s"""
61
63
 
62
64
 
63
 
_FTP_cache = {}
64
 
def _find_FTP(hostname, port, username, password, is_active):
65
 
    """Find an ftplib.FTP instance attached to this triplet."""
66
 
    key = (hostname, port, username, password, is_active)
67
 
    alt_key = (hostname, port, username, '********', is_active)
68
 
    if key not in _FTP_cache:
69
 
        mutter("Constructing FTP instance against %r" % (alt_key,))
70
 
        conn = ftplib.FTP()
71
 
 
72
 
        conn.connect(host=hostname, port=port)
73
 
        if username and username != 'anonymous' and not password:
74
 
            password = bzrlib.ui.ui_factory.get_password(
75
 
                prompt='FTP %(user)s@%(host)s password',
76
 
                user=username, host=hostname)
77
 
        conn.login(user=username, passwd=password)
78
 
        conn.set_pasv(not is_active)
79
 
 
80
 
        _FTP_cache[key] = conn
81
 
 
82
 
    return _FTP_cache[key]    
83
 
 
84
 
 
85
65
class FtpStatResult(object):
86
66
    def __init__(self, f, relpath):
87
67
        try:
99
79
_number_of_retries = 2
100
80
_sleep_between_retries = 5
101
81
 
102
 
class FtpTransport(Transport):
 
82
# FIXME: there are inconsistencies in the way temporary errors are
 
83
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
 
84
# be taken to analyze the implications for write operations (read operations
 
85
# are safe to retry). Overall even some read operations are never
 
86
# retried. --vila 20070720 (Bug #127164)
 
87
class FtpTransport(ConnectedTransport):
103
88
    """This is the transport agent for ftp:// access."""
104
89
 
105
 
    def __init__(self, base, _provided_instance=None):
 
90
    def __init__(self, base, _from_transport=None):
106
91
        """Set the base path where files will be stored."""
107
92
        assert base.startswith('ftp://') or base.startswith('aftp://')
108
 
 
109
 
        self.is_active = base.startswith('aftp://')
110
 
        if self.is_active:
111
 
            # urlparse won't handle aftp://
112
 
            base = base[1:]
113
 
        if not base.endswith('/'):
114
 
            base += '/'
115
 
        (self._proto, self._username,
116
 
            self._password, self._host,
117
 
            self._port, self._path) = split_url(base)
118
 
        base = self._unparse_url()
119
 
 
120
 
        super(FtpTransport, self).__init__(base)
121
 
        self._FTP_instance = _provided_instance
122
 
 
123
 
    def _unparse_url(self, path=None):
124
 
        if path is None:
125
 
            path = self._path
126
 
        path = urllib.quote(path)
127
 
        netloc = urllib.quote(self._host)
128
 
        if self._username is not None:
129
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
130
 
        if self._port is not None:
131
 
            netloc = '%s:%d' % (netloc, self._port)
132
 
        proto = 'ftp'
133
 
        if self.is_active:
134
 
            proto = 'aftp'
135
 
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
 
93
        super(FtpTransport, self).__init__(base,
 
94
                                           _from_transport=_from_transport)
 
95
        self._unqualified_scheme = 'ftp'
 
96
        if self._scheme == 'aftp':
 
97
            self.is_active = True
 
98
        else:
 
99
            self.is_active = False
136
100
 
137
101
    def _get_FTP(self):
138
102
        """Return the ftplib.FTP instance for this object."""
139
 
        if self._FTP_instance is not None:
140
 
            return self._FTP_instance
141
 
        
 
103
        # Ensures that a connection is established
 
104
        connection = self._get_connection()
 
105
        if connection is None:
 
106
            # First connection ever
 
107
            connection, credentials = self._create_connection()
 
108
            self._set_connection(connection, credentials)
 
109
        return connection
 
110
 
 
111
    def _create_connection(self, credentials=None):
 
112
        """Create a new connection with the provided credentials.
 
113
 
 
114
        :param credentials: The credentials needed to establish the connection.
 
115
 
 
116
        :return: The created connection and its associated credentials.
 
117
 
 
118
        The credentials are only the password as it may have been entered
 
119
        interactively by the user and may be different from the one provided
 
120
        in base url at transport creation time.
 
121
        """
 
122
        if credentials is None:
 
123
            user, password = self._user, self._password
 
124
        else:
 
125
            user, password = credentials
 
126
 
 
127
        auth = config.AuthenticationConfig()
 
128
        if user is None:
 
129
            user = auth.get_user('ftp', self._host, port=self._port)
 
130
            if user is None:
 
131
                # Default to local user
 
132
                user = getpass.getuser()
 
133
 
 
134
        mutter("Constructing FTP instance against %r" %
 
135
               ((self._host, self._port, user, '********',
 
136
                self.is_active),))
142
137
        try:
143
 
            self._FTP_instance = _find_FTP(self._host, self._port,
144
 
                                           self._username, self._password,
145
 
                                           self.is_active)
146
 
            return self._FTP_instance
 
138
            connection = ftplib.FTP()
 
139
            connection.connect(host=self._host, port=self._port)
 
140
            if user and user != 'anonymous' and \
 
141
                    password is None: # '' is a valid password
 
142
                password = auth.get_password('ftp', self._host, user,
 
143
                                             port=self._port)
 
144
            connection.login(user=user, passwd=password)
 
145
            connection.set_pasv(not self.is_active)
 
146
        except socket.error, e:
 
147
            raise errors.SocketConnectionError(self._host, self._port,
 
148
                                               msg='Unable to connect to',
 
149
                                               orig_error= e)
147
150
        except ftplib.error_perm, e:
148
 
            raise errors.TransportError(msg="Error setting up connection: %s"
149
 
                                    % str(e), orig_error=e)
150
 
 
151
 
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
 
151
            raise errors.TransportError(msg="Error setting up connection:"
 
152
                                        " %s" % str(e), orig_error=e)
 
153
        return connection, (user, password)
 
154
 
 
155
    def _reconnect(self):
 
156
        """Create a new connection with the previously used credentials"""
 
157
        credentials = self._get_credentials()
 
158
        connection, credentials = self._create_connection(credentials)
 
159
        self._set_connection(connection, credentials)
 
160
 
 
161
    def _translate_perm_error(self, err, path, extra=None,
 
162
                              unknown_exc=FtpPathError):
152
163
        """Try to translate an ftplib.error_perm exception.
153
164
 
154
165
        :param err: The error to translate into a bzr error
166
177
            or 'could not open' in s
167
178
            or 'no such dir' in s
168
179
            or 'could not create file' in s # vsftpd
 
180
            or 'file doesn\'t exist' in s
 
181
            or 'file/directory not found' in s # filezilla server
169
182
            ):
170
183
            raise errors.NoSuchFile(path, extra=extra)
171
184
        if ('file exists' in s):
184
197
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
185
198
        raise
186
199
 
187
 
    def should_cache(self):
188
 
        """Return True if the data pulled across should be cached locally.
189
 
        """
190
 
        return True
191
 
 
192
 
    def clone(self, offset=None):
193
 
        """Return a new FtpTransport with root at self.base + offset.
194
 
        """
195
 
        mutter("FTP clone")
196
 
        if offset is None:
197
 
            return FtpTransport(self.base, self._FTP_instance)
198
 
        else:
199
 
            return FtpTransport(self.abspath(offset), self._FTP_instance)
200
 
 
201
 
    def _abspath(self, relpath):
202
 
        assert isinstance(relpath, basestring)
203
 
        relpath = urlutils.unescape(relpath)
204
 
        if relpath.startswith('/'):
205
 
            basepath = []
206
 
        else:
207
 
            basepath = self._path.split('/')
208
 
        if len(basepath) > 0 and basepath[-1] == '':
209
 
            basepath = basepath[:-1]
210
 
        for p in relpath.split('/'):
211
 
            if p == '..':
212
 
                if len(basepath) == 0:
213
 
                    # In most filesystems, a request for the parent
214
 
                    # of root, just returns root.
215
 
                    continue
216
 
                basepath.pop()
217
 
            elif p == '.' or p == '':
218
 
                continue # No-op
219
 
            else:
220
 
                basepath.append(p)
221
 
        # Possibly, we could use urlparse.urljoin() here, but
222
 
        # I'm concerned about when it chooses to strip the last
223
 
        # portion of the path, and when it doesn't.
224
 
 
 
200
    def _remote_path(self, relpath):
225
201
        # XXX: It seems that ftplib does not handle Unicode paths
226
 
        # at the same time, medusa won't handle utf8 paths
227
 
        # So if we .encode(utf8) here, then we get a Server failure.
228
 
        # while if we use str(), we get a UnicodeError, and the test suite
229
 
        # just skips testing UnicodePaths.
230
 
        return str('/'.join(basepath) or '/')
231
 
    
232
 
    def abspath(self, relpath):
233
 
        """Return the full url to the given relative path.
234
 
        This can be supplied with a string or a list
235
 
        """
236
 
        path = self._abspath(relpath)
237
 
        return self._unparse_url(path)
 
202
        # at the same time, medusa won't handle utf8 paths So if
 
203
        # we .encode(utf8) here (see ConnectedTransport
 
204
        # implementation), then we get a Server failure.  while
 
205
        # if we use str(), we get a UnicodeError, and the test
 
206
        # suite just skips testing UnicodePaths.
 
207
        relative = str(urlutils.unescape(relpath))
 
208
        remote_path = self._combine_paths(self._path, relative)
 
209
        return remote_path
238
210
 
239
211
    def has(self, relpath):
240
212
        """Does the target location exist?"""
243
215
        # XXX: I assume we're never asked has(dirname) and thus I use
244
216
        # the FTP size command and assume that if it doesn't raise,
245
217
        # all is good.
246
 
        abspath = self._abspath(relpath)
 
218
        abspath = self._remote_path(relpath)
247
219
        try:
248
220
            f = self._get_FTP()
249
221
            mutter('FTP has check: %s => %s', relpath, abspath)
269
241
        """
270
242
        # TODO: decode should be deprecated
271
243
        try:
272
 
            mutter("FTP get: %s", self._abspath(relpath))
 
244
            mutter("FTP get: %s", self._remote_path(relpath))
273
245
            f = self._get_FTP()
274
246
            ret = StringIO()
275
 
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
 
247
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
276
248
            ret.seek(0)
277
249
            return ret
278
250
        except ftplib.error_perm, e:
284
256
                                     orig_error=e)
285
257
            else:
286
258
                warning("FTP temporary error: %s. Retrying.", str(e))
287
 
                self._FTP_instance = None
 
259
                self._reconnect()
288
260
                return self.get(relpath, decode, retries+1)
289
261
        except EOFError, e:
290
262
            if retries > _number_of_retries:
294
266
            else:
295
267
                warning("FTP control connection closed. Trying to reopen.")
296
268
                time.sleep(_sleep_between_retries)
297
 
                self._FTP_instance = None
 
269
                self._reconnect()
298
270
                return self.get(relpath, decode, retries+1)
299
271
 
300
272
    def put_file(self, relpath, fp, mode=None, retries=0):
308
280
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
309
281
        ftplib does not
310
282
        """
311
 
        abspath = self._abspath(relpath)
 
283
        abspath = self._remote_path(relpath)
312
284
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
313
285
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
286
        bytes = None
314
287
        if getattr(fp, 'read', None) is None:
315
 
            fp = StringIO(fp)
 
288
            # hand in a string IO
 
289
            bytes = fp
 
290
            fp = StringIO(bytes)
 
291
        else:
 
292
            # capture the byte count; .read() may be read only so
 
293
            # decorate it.
 
294
            class byte_counter(object):
 
295
                def __init__(self, fp):
 
296
                    self.fp = fp
 
297
                    self.counted_bytes = 0
 
298
                def read(self, count):
 
299
                    result = self.fp.read(count)
 
300
                    self.counted_bytes += len(result)
 
301
                    return result
 
302
            fp = byte_counter(fp)
316
303
        try:
317
304
            mutter("FTP put: %s", abspath)
318
305
            f = self._get_FTP()
319
306
            try:
320
307
                f.storbinary('STOR '+tmp_abspath, fp)
321
308
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
309
                if bytes is not None:
 
310
                    return len(bytes)
 
311
                else:
 
312
                    return fp.counted_bytes
322
313
            except (ftplib.error_temp,EOFError), e:
323
314
                warning("Failure during ftp PUT. Deleting temporary file.")
324
315
                try:
329
320
                    raise e
330
321
                raise
331
322
        except ftplib.error_perm, e:
332
 
            self._translate_perm_error(e, abspath, extra='could not store')
 
323
            self._translate_perm_error(e, abspath, extra='could not store',
 
324
                                       unknown_exc=errors.NoSuchFile)
333
325
        except ftplib.error_temp, e:
334
326
            if retries > _number_of_retries:
335
327
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
336
328
                                     % self.abspath(relpath), orig_error=e)
337
329
            else:
338
330
                warning("FTP temporary error: %s. Retrying.", str(e))
339
 
                self._FTP_instance = None
 
331
                self._reconnect()
340
332
                self.put_file(relpath, fp, mode, retries+1)
341
333
        except EOFError:
342
334
            if retries > _number_of_retries:
345
337
            else:
346
338
                warning("FTP control connection closed. Trying to reopen.")
347
339
                time.sleep(_sleep_between_retries)
348
 
                self._FTP_instance = None
 
340
                self._reconnect()
349
341
                self.put_file(relpath, fp, mode, retries+1)
350
342
 
351
343
    def mkdir(self, relpath, mode=None):
352
344
        """Create a directory at the given path."""
353
 
        abspath = self._abspath(relpath)
 
345
        abspath = self._remote_path(relpath)
354
346
        try:
355
347
            mutter("FTP mkd: %s", abspath)
356
348
            f = self._get_FTP()
359
351
            self._translate_perm_error(e, abspath,
360
352
                unknown_exc=errors.FileExists)
361
353
 
 
354
    def open_write_stream(self, relpath, mode=None):
 
355
        """See Transport.open_write_stream."""
 
356
        self.put_bytes(relpath, "", mode)
 
357
        result = AppendBasedFileStream(self, relpath)
 
358
        _file_streams[self.abspath(relpath)] = result
 
359
        return result
 
360
 
 
361
    def recommended_page_size(self):
 
362
        """See Transport.recommended_page_size().
 
363
 
 
364
        For FTP we suggest a large page size to reduce the overhead
 
365
        introduced by latency.
 
366
        """
 
367
        return 64 * 1024
 
368
 
362
369
    def rmdir(self, rel_path):
363
370
        """Delete the directory at rel_path"""
364
 
        abspath = self._abspath(rel_path)
 
371
        abspath = self._remote_path(rel_path)
365
372
        try:
366
373
            mutter("FTP rmd: %s", abspath)
367
374
            f = self._get_FTP()
373
380
        """Append the text in the file-like object into the final
374
381
        location.
375
382
        """
376
 
        abspath = self._abspath(relpath)
 
383
        abspath = self._remote_path(relpath)
377
384
        if self.has(relpath):
378
385
            ftp = self._get_FTP()
379
386
            result = ftp.size(abspath)
392
399
        number of retries is exceeded.
393
400
        """
394
401
        try:
395
 
            abspath = self._abspath(relpath)
 
402
            abspath = self._remote_path(relpath)
396
403
            mutter("FTP appe (try %d) to %s", retries, abspath)
397
404
            ftp = self._get_FTP()
398
405
            ftp.voidcmd("TYPE I")
412
419
                        "Aborting." % abspath, orig_error=e)
413
420
            else:
414
421
                warning("FTP temporary error: %s. Retrying.", str(e))
415
 
                self._FTP_instance = None
 
422
                self._reconnect()
416
423
                self._try_append(relpath, text, mode, retries+1)
417
424
 
418
425
    def _setmode(self, relpath, mode):
423
430
        """
424
431
        try:
425
432
            mutter("FTP site chmod: setting permissions to %s on %s",
426
 
                str(mode), self._abspath(relpath))
 
433
                str(mode), self._remote_path(relpath))
427
434
            ftp = self._get_FTP()
428
 
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
 
435
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
429
436
            ftp.sendcmd(cmd)
430
437
        except ftplib.error_perm, e:
431
438
            # Command probably not available on this server
432
439
            warning("FTP Could not set permissions to %s on %s. %s",
433
 
                    str(mode), self._abspath(relpath), str(e))
 
440
                    str(mode), self._remote_path(relpath), str(e))
434
441
 
435
442
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
436
443
    #       to copy something to another machine. And you may be able
438
445
    #       So implement a fancier 'copy()'
439
446
 
440
447
    def rename(self, rel_from, rel_to):
441
 
        abs_from = self._abspath(rel_from)
442
 
        abs_to = self._abspath(rel_to)
 
448
        abs_from = self._remote_path(rel_from)
 
449
        abs_to = self._remote_path(rel_to)
443
450
        mutter("FTP rename: %s => %s", abs_from, abs_to)
444
451
        f = self._get_FTP()
445
452
        return self._rename(abs_from, abs_to, f)
453
460
 
454
461
    def move(self, rel_from, rel_to):
455
462
        """Move the item at rel_from to the location at rel_to"""
456
 
        abs_from = self._abspath(rel_from)
457
 
        abs_to = self._abspath(rel_to)
 
463
        abs_from = self._remote_path(rel_from)
 
464
        abs_to = self._remote_path(rel_to)
458
465
        try:
459
466
            mutter("FTP mv: %s => %s", abs_from, abs_to)
460
467
            f = self._get_FTP()
475
482
 
476
483
    def delete(self, relpath):
477
484
        """Delete the item at relpath"""
478
 
        abspath = self._abspath(relpath)
 
485
        abspath = self._remote_path(relpath)
479
486
        f = self._get_FTP()
480
487
        self._delete(abspath, f)
481
488
 
487
494
            self._translate_perm_error(e, abspath, 'error deleting',
488
495
                unknown_exc=errors.NoSuchFile)
489
496
 
 
497
    def external_url(self):
 
498
        """See bzrlib.transport.Transport.external_url."""
 
499
        # FTP URL's are externally usable.
 
500
        return self.base
 
501
 
490
502
    def listable(self):
491
503
        """See Transport.listable."""
492
504
        return True
493
505
 
494
506
    def list_dir(self, relpath):
495
507
        """See Transport.list_dir."""
496
 
        basepath = self._abspath(relpath)
 
508
        basepath = self._remote_path(relpath)
497
509
        mutter("FTP nlst: %s", basepath)
498
510
        f = self._get_FTP()
499
511
        try:
526
538
 
527
539
    def stat(self, relpath):
528
540
        """Return the stat information for a file."""
529
 
        abspath = self._abspath(relpath)
 
541
        abspath = self._remote_path(relpath)
530
542
        try:
531
543
            mutter("FTP stat: %s", abspath)
532
544
            f = self._get_FTP()
556
568
        return self.lock_read(relpath)
557
569
 
558
570
 
559
 
class FtpServer(Server):
560
 
    """Common code for SFTP server facilities."""
561
 
 
562
 
    def __init__(self):
563
 
        self._root = None
564
 
        self._ftp_server = None
565
 
        self._port = None
566
 
        self._async_thread = None
567
 
        # ftp server logs
568
 
        self.logs = []
569
 
 
570
 
    def get_url(self):
571
 
        """Calculate an ftp url to this server."""
572
 
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
573
 
 
574
 
#    def get_bogus_url(self):
575
 
#        """Return a URL which cannot be connected to."""
576
 
#        return 'ftp://127.0.0.1:1'
577
 
 
578
 
    def log(self, message):
579
 
        """This is used by medusa.ftp_server to log connections, etc."""
580
 
        self.logs.append(message)
581
 
 
582
 
    def setUp(self, vfs_server=None):
583
 
        if not _have_medusa:
584
 
            raise RuntimeError('Must have medusa to run the FtpServer')
585
 
 
586
 
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
587
 
            "FtpServer currently assumes local transport, got %s" % vfs_server
588
 
 
589
 
        self._root = os.getcwdu()
590
 
        self._ftp_server = _ftp_server(
591
 
            authorizer=_test_authorizer(root=self._root),
592
 
            ip='localhost',
593
 
            port=0, # bind to a random port
594
 
            resolver=None,
595
 
            logger_object=self # Use FtpServer.log() for messages
596
 
            )
597
 
        self._port = self._ftp_server.getsockname()[1]
598
 
        # Don't let it loop forever, or handle an infinite number of requests.
599
 
        # In this case it will run for 100s, or 1000 requests
600
 
        self._async_thread = threading.Thread(
601
 
                target=FtpServer._asyncore_loop_ignore_EBADF,
602
 
                kwargs={'timeout':0.1, 'count':1000})
603
 
        self._async_thread.setDaemon(True)
604
 
        self._async_thread.start()
605
 
 
606
 
    def tearDown(self):
607
 
        """See bzrlib.transport.Server.tearDown."""
608
 
        # have asyncore release the channel
609
 
        self._ftp_server.del_channel()
610
 
        asyncore.close_all()
611
 
        self._async_thread.join()
612
 
 
613
 
    @staticmethod
614
 
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
615
 
        """Ignore EBADF during server shutdown.
616
 
 
617
 
        We close the socket to get the server to shutdown, but this causes
618
 
        select.select() to raise EBADF.
619
 
        """
620
 
        try:
621
 
            asyncore.loop(*args, **kwargs)
622
 
        except select.error, e:
623
 
            if e.args[0] != errno.EBADF:
624
 
                raise
625
 
 
626
 
 
627
 
_ftp_channel = None
628
 
_ftp_server = None
629
 
_test_authorizer = None
630
 
 
631
 
 
632
 
def _setup_medusa():
633
 
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
634
 
    try:
635
 
        import medusa
636
 
        import medusa.filesys
637
 
        import medusa.ftp_server
638
 
    except ImportError:
639
 
        return False
640
 
 
641
 
    _have_medusa = True
642
 
 
643
 
    class test_authorizer(object):
644
 
        """A custom Authorizer object for running the test suite.
645
 
 
646
 
        The reason we cannot use dummy_authorizer, is because it sets the
647
 
        channel to readonly, which we don't always want to do.
648
 
        """
649
 
 
650
 
        def __init__(self, root):
651
 
            self.root = root
652
 
 
653
 
        def authorize(self, channel, username, password):
654
 
            """Return (success, reply_string, filesystem)"""
655
 
            if not _have_medusa:
656
 
                return 0, 'No Medusa.', None
657
 
 
658
 
            channel.persona = -1, -1
659
 
            if username == 'anonymous':
660
 
                channel.read_only = 1
661
 
            else:
662
 
                channel.read_only = 0
663
 
 
664
 
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
665
 
 
666
 
 
667
 
    class ftp_channel(medusa.ftp_server.ftp_channel):
668
 
        """Customized ftp channel"""
669
 
 
670
 
        def log(self, message):
671
 
            """Redirect logging requests."""
672
 
            mutter('_ftp_channel: %s', message)
673
 
            
674
 
        def log_info(self, message, type='info'):
675
 
            """Redirect logging requests."""
676
 
            mutter('_ftp_channel %s: %s', type, message)
677
 
            
678
 
        def cmd_rnfr(self, line):
679
 
            """Prepare for renaming a file."""
680
 
            self._renaming = line[1]
681
 
            self.respond('350 Ready for RNTO')
682
 
            # TODO: jam 20060516 in testing, the ftp server seems to
683
 
            #       check that the file already exists, or it sends
684
 
            #       550 RNFR command failed
685
 
 
686
 
        def cmd_rnto(self, line):
687
 
            """Rename a file based on the target given.
688
 
 
689
 
            rnto must be called after calling rnfr.
690
 
            """
691
 
            if not self._renaming:
692
 
                self.respond('503 RNFR required first.')
693
 
            pfrom = self.filesystem.translate(self._renaming)
694
 
            self._renaming = None
695
 
            pto = self.filesystem.translate(line[1])
696
 
            if os.path.exists(pto):
697
 
                self.respond('550 RNTO failed: file exists')
698
 
                return
699
 
            try:
700
 
                os.rename(pfrom, pto)
701
 
            except (IOError, OSError), e:
702
 
                # TODO: jam 20060516 return custom responses based on
703
 
                #       why the command failed
704
 
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
705
 
                # sometimes don't provide expected error message;
706
 
                # so we obtain such message via os.strerror()
707
 
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
708
 
            except:
709
 
                self.respond('550 RNTO failed')
710
 
                # For a test server, we will go ahead and just die
711
 
                raise
712
 
            else:
713
 
                self.respond('250 Rename successful.')
714
 
 
715
 
        def cmd_size(self, line):
716
 
            """Return the size of a file
717
 
 
718
 
            This is overloaded to help the test suite determine if the 
719
 
            target is a directory.
720
 
            """
721
 
            filename = line[1]
722
 
            if not self.filesystem.isfile(filename):
723
 
                if self.filesystem.isdir(filename):
724
 
                    self.respond('550 "%s" is a directory' % (filename,))
725
 
                else:
726
 
                    self.respond('550 "%s" is not a file' % (filename,))
727
 
            else:
728
 
                self.respond('213 %d' 
729
 
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
730
 
 
731
 
        def cmd_mkd(self, line):
732
 
            """Create a directory.
733
 
 
734
 
            Overloaded because default implementation does not distinguish
735
 
            *why* it cannot make a directory.
736
 
            """
737
 
            if len (line) != 2:
738
 
                self.command_not_understood(''.join(line))
739
 
            else:
740
 
                path = line[1]
741
 
                try:
742
 
                    self.filesystem.mkdir (path)
743
 
                    self.respond ('257 MKD command successful.')
744
 
                except (IOError, OSError), e:
745
 
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
746
 
                    # sometimes don't provide expected error message;
747
 
                    # so we obtain such message via os.strerror()
748
 
                    self.respond ('550 error creating directory: %s' %
749
 
                                  os.strerror(e.errno))
750
 
                except:
751
 
                    self.respond ('550 error creating directory.')
752
 
 
753
 
 
754
 
    class ftp_server(medusa.ftp_server.ftp_server):
755
 
        """Customize the behavior of the Medusa ftp_server.
756
 
 
757
 
        There are a few warts on the ftp_server, based on how it expects
758
 
        to be used.
759
 
        """
760
 
        _renaming = None
761
 
        ftp_channel_class = ftp_channel
762
 
 
763
 
        def __init__(self, *args, **kwargs):
764
 
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
765
 
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
766
 
 
767
 
        def log(self, message):
768
 
            """Redirect logging requests."""
769
 
            mutter('_ftp_server: %s', message)
770
 
 
771
 
        def log_info(self, message, type='info'):
772
 
            """Override the asyncore.log_info so we don't stipple the screen."""
773
 
            mutter('_ftp_server %s: %s', type, message)
774
 
 
775
 
    _test_authorizer = test_authorizer
776
 
    _ftp_channel = ftp_channel
777
 
    _ftp_server = ftp_server
778
 
 
779
 
    return True
780
 
 
781
 
 
782
571
def get_test_permutations():
783
572
    """Return the permutations to be used in testing."""
784
 
    if not _setup_medusa():
785
 
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
786
 
        return []
 
573
    from bzrlib import tests
 
574
    if tests.FTPServerFeature.available():
 
575
        from bzrlib.tests import ftp_server
 
576
        return [(FtpTransport, ftp_server.FTPServer)]
787
577
    else:
788
 
        return [(FtpTransport, FtpServer)]
 
578
        # Dummy server to have the test suite report the number of tests
 
579
        # needing that feature.
 
580
        class UnavailableFTPServer(object):
 
581
            def setUp(self):
 
582
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
583
 
 
584
        return [(FtpTransport, UnavailableFTPServer)]