/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

First attempt to merge .dev and resolve the conflicts (but tests are 
failing)

Show diffs side-by-side

added added

removed removed

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