/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/__init__.py

Merge with serialize-transform

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
"""Implementation of Transport over ftp.
 
17
 
 
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
 
19
cargo-culting from the sftp transport and the http transport.
 
20
 
 
21
It provides the ftp:// and aftp:// protocols where ftp:// is passive ftp
 
22
and aftp:// is active ftp. Most people will want passive ftp for traversing
 
23
NAT and other firewalls, so it's best to use it unless you explicitly want
 
24
active, in which case aftp:// will be your friend.
 
25
"""
 
26
 
 
27
from cStringIO import StringIO
 
28
import errno
 
29
import ftplib
 
30
import getpass
 
31
import os
 
32
import os.path
 
33
import urlparse
 
34
import socket
 
35
import stat
 
36
import time
 
37
import random
 
38
from warnings import warn
 
39
 
 
40
from bzrlib import (
 
41
    config,
 
42
    errors,
 
43
    osutils,
 
44
    urlutils,
 
45
    )
 
46
from bzrlib.trace import mutter, warning
 
47
from bzrlib.transport import (
 
48
    AppendBasedFileStream,
 
49
    ConnectedTransport,
 
50
    _file_streams,
 
51
    register_urlparse_netloc_protocol,
 
52
    Server,
 
53
    )
 
54
from bzrlib.transport.local import LocalURLServer
 
55
import bzrlib.ui
 
56
 
 
57
 
 
58
register_urlparse_netloc_protocol('aftp')
 
59
 
 
60
 
 
61
class FtpPathError(errors.PathError):
 
62
    """FTP failed for path: %(path)s%(extra)s"""
 
63
 
 
64
 
 
65
class FtpStatResult(object):
 
66
    def __init__(self, f, relpath):
 
67
        try:
 
68
            self.st_size = f.size(relpath)
 
69
            self.st_mode = stat.S_IFREG
 
70
        except ftplib.error_perm:
 
71
            pwd = f.pwd()
 
72
            try:
 
73
                f.cwd(relpath)
 
74
                self.st_mode = stat.S_IFDIR
 
75
            finally:
 
76
                f.cwd(pwd)
 
77
 
 
78
 
 
79
_number_of_retries = 2
 
80
_sleep_between_retries = 5
 
81
 
 
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):
 
88
    """This is the transport agent for ftp:// access."""
 
89
 
 
90
    def __init__(self, base, _from_transport=None):
 
91
        """Set the base path where files will be stored."""
 
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
 
101
 
 
102
    def _get_FTP(self):
 
103
        """Return the ftplib.FTP instance for this object."""
 
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 input credentials are only the password as it may have been
 
120
        entered interactively by the user and may be different from the one
 
121
        provided in base url at transport creation time.  The returned
 
122
        credentials are username, password.
 
123
        """
 
124
        if credentials is None:
 
125
            user, password = self._user, self._password
 
126
        else:
 
127
            user, password = credentials
 
128
 
 
129
        auth = config.AuthenticationConfig()
 
130
        if user is None:
 
131
            user = auth.get_user('ftp', self._host, port=self._port)
 
132
            if user is None:
 
133
                # Default to local user
 
134
                user = getpass.getuser()
 
135
 
 
136
        mutter("Constructing FTP instance against %r" %
 
137
               ((self._host, self._port, user, '********',
 
138
                self.is_active),))
 
139
        try:
 
140
            connection = ftplib.FTP()
 
141
            connection.connect(host=self._host, port=self._port)
 
142
            if user and user != 'anonymous' and \
 
143
                    password is None: # '' is a valid password
 
144
                password = auth.get_password('ftp', self._host, user,
 
145
                                             port=self._port)
 
146
            connection.login(user=user, passwd=password)
 
147
            connection.set_pasv(not self.is_active)
 
148
        except socket.error, e:
 
149
            raise errors.SocketConnectionError(self._host, self._port,
 
150
                                               msg='Unable to connect to',
 
151
                                               orig_error= e)
 
152
        except ftplib.error_perm, e:
 
153
            raise errors.TransportError(msg="Error setting up connection:"
 
154
                                        " %s" % str(e), orig_error=e)
 
155
        return connection, (user, password)
 
156
 
 
157
    def _reconnect(self):
 
158
        """Create a new connection with the previously used credentials"""
 
159
        credentials = self._get_credentials()
 
160
        connection, credentials = self._create_connection(credentials)
 
161
        self._set_connection(connection, credentials)
 
162
 
 
163
    def _translate_perm_error(self, err, path, extra=None,
 
164
                              unknown_exc=FtpPathError):
 
165
        """Try to translate an ftplib.error_perm exception.
 
166
 
 
167
        :param err: The error to translate into a bzr error
 
168
        :param path: The path which had problems
 
169
        :param extra: Extra information which can be included
 
170
        :param unknown_exc: If None, we will just raise the original exception
 
171
                    otherwise we raise unknown_exc(path, extra=extra)
 
172
        """
 
173
        s = str(err).lower()
 
174
        if not extra:
 
175
            extra = str(err)
 
176
        else:
 
177
            extra += ': ' + str(err)
 
178
        if ('no such file' in s
 
179
            or 'could not open' in s
 
180
            or 'no such dir' in s
 
181
            or 'could not create file' in s # vsftpd
 
182
            or 'file doesn\'t exist' in s
 
183
            or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
 
184
            or 'file/directory not found' in s # filezilla server
 
185
            # Microsoft FTP-Service RNFR reply if file not found
 
186
            or (s.startswith('550 ') and 'unable to rename to' in extra)
 
187
            ):
 
188
            raise errors.NoSuchFile(path, extra=extra)
 
189
        if ('file exists' in s):
 
190
            raise errors.FileExists(path, extra=extra)
 
191
        if ('not a directory' in s):
 
192
            raise errors.PathError(path, extra=extra)
 
193
 
 
194
        mutter('unable to understand error for path: %s: %s', path, err)
 
195
 
 
196
        if unknown_exc:
 
197
            raise unknown_exc(path, extra=extra)
 
198
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
 
199
        #       something like TransportError, but this loses the traceback
 
200
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
 
201
        #       to handle. Consider doing something like that here.
 
202
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
 
203
        raise
 
204
 
 
205
    def _remote_path(self, relpath):
 
206
        # XXX: It seems that ftplib does not handle Unicode paths
 
207
        # at the same time, medusa won't handle utf8 paths So if
 
208
        # we .encode(utf8) here (see ConnectedTransport
 
209
        # implementation), then we get a Server failure.  while
 
210
        # if we use str(), we get a UnicodeError, and the test
 
211
        # suite just skips testing UnicodePaths.
 
212
        relative = str(urlutils.unescape(relpath))
 
213
        remote_path = self._combine_paths(self._path, relative)
 
214
        return remote_path
 
215
 
 
216
    def has(self, relpath):
 
217
        """Does the target location exist?"""
 
218
        # FIXME jam 20060516 We *do* ask about directories in the test suite
 
219
        #       We don't seem to in the actual codebase
 
220
        # XXX: I assume we're never asked has(dirname) and thus I use
 
221
        # the FTP size command and assume that if it doesn't raise,
 
222
        # all is good.
 
223
        abspath = self._remote_path(relpath)
 
224
        try:
 
225
            f = self._get_FTP()
 
226
            mutter('FTP has check: %s => %s', relpath, abspath)
 
227
            s = f.size(abspath)
 
228
            mutter("FTP has: %s", abspath)
 
229
            return True
 
230
        except ftplib.error_perm, e:
 
231
            if ('is a directory' in str(e).lower()):
 
232
                mutter("FTP has dir: %s: %s", abspath, e)
 
233
                return True
 
234
            mutter("FTP has not: %s: %s", abspath, e)
 
235
            return False
 
236
 
 
237
    def get(self, relpath, decode=False, retries=0):
 
238
        """Get the file at the given relative path.
 
239
 
 
240
        :param relpath: The relative path to the file
 
241
        :param retries: Number of retries after temporary failures so far
 
242
                        for this operation.
 
243
 
 
244
        We're meant to return a file-like object which bzr will
 
245
        then read from. For now we do this via the magic of StringIO
 
246
        """
 
247
        # TODO: decode should be deprecated
 
248
        try:
 
249
            mutter("FTP get: %s", self._remote_path(relpath))
 
250
            f = self._get_FTP()
 
251
            ret = StringIO()
 
252
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
 
253
            ret.seek(0)
 
254
            return ret
 
255
        except ftplib.error_perm, e:
 
256
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
 
257
        except ftplib.error_temp, e:
 
258
            if retries > _number_of_retries:
 
259
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
 
260
                                     % self.abspath(relpath),
 
261
                                     orig_error=e)
 
262
            else:
 
263
                warning("FTP temporary error: %s. Retrying.", str(e))
 
264
                self._reconnect()
 
265
                return self.get(relpath, decode, retries+1)
 
266
        except EOFError, e:
 
267
            if retries > _number_of_retries:
 
268
                raise errors.TransportError("FTP control connection closed during GET %s."
 
269
                                     % self.abspath(relpath),
 
270
                                     orig_error=e)
 
271
            else:
 
272
                warning("FTP control connection closed. Trying to reopen.")
 
273
                time.sleep(_sleep_between_retries)
 
274
                self._reconnect()
 
275
                return self.get(relpath, decode, retries+1)
 
276
 
 
277
    def put_file(self, relpath, fp, mode=None, retries=0):
 
278
        """Copy the file-like or string object into the location.
 
279
 
 
280
        :param relpath: Location to put the contents, relative to base.
 
281
        :param fp:       File-like or string object.
 
282
        :param retries: Number of retries after temporary failures so far
 
283
                        for this operation.
 
284
 
 
285
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
 
286
        ftplib does not
 
287
        """
 
288
        abspath = self._remote_path(relpath)
 
289
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
 
290
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
291
        bytes = None
 
292
        if getattr(fp, 'read', None) is None:
 
293
            # hand in a string IO
 
294
            bytes = fp
 
295
            fp = StringIO(bytes)
 
296
        else:
 
297
            # capture the byte count; .read() may be read only so
 
298
            # decorate it.
 
299
            class byte_counter(object):
 
300
                def __init__(self, fp):
 
301
                    self.fp = fp
 
302
                    self.counted_bytes = 0
 
303
                def read(self, count):
 
304
                    result = self.fp.read(count)
 
305
                    self.counted_bytes += len(result)
 
306
                    return result
 
307
            fp = byte_counter(fp)
 
308
        try:
 
309
            mutter("FTP put: %s", abspath)
 
310
            f = self._get_FTP()
 
311
            try:
 
312
                f.storbinary('STOR '+tmp_abspath, fp)
 
313
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
314
                self._setmode(relpath, mode)
 
315
                if bytes is not None:
 
316
                    return len(bytes)
 
317
                else:
 
318
                    return fp.counted_bytes
 
319
            except (ftplib.error_temp,EOFError), e:
 
320
                warning("Failure during ftp PUT. Deleting temporary file.")
 
321
                try:
 
322
                    f.delete(tmp_abspath)
 
323
                except:
 
324
                    warning("Failed to delete temporary file on the"
 
325
                            " server.\nFile: %s", tmp_abspath)
 
326
                    raise e
 
327
                raise
 
328
        except ftplib.error_perm, e:
 
329
            self._translate_perm_error(e, abspath, extra='could not store',
 
330
                                       unknown_exc=errors.NoSuchFile)
 
331
        except ftplib.error_temp, e:
 
332
            if retries > _number_of_retries:
 
333
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
 
334
                                     % self.abspath(relpath), orig_error=e)
 
335
            else:
 
336
                warning("FTP temporary error: %s. Retrying.", str(e))
 
337
                self._reconnect()
 
338
                self.put_file(relpath, fp, mode, retries+1)
 
339
        except EOFError:
 
340
            if retries > _number_of_retries:
 
341
                raise errors.TransportError("FTP control connection closed during PUT %s."
 
342
                                     % self.abspath(relpath), orig_error=e)
 
343
            else:
 
344
                warning("FTP control connection closed. Trying to reopen.")
 
345
                time.sleep(_sleep_between_retries)
 
346
                self._reconnect()
 
347
                self.put_file(relpath, fp, mode, retries+1)
 
348
 
 
349
    def mkdir(self, relpath, mode=None):
 
350
        """Create a directory at the given path."""
 
351
        abspath = self._remote_path(relpath)
 
352
        try:
 
353
            mutter("FTP mkd: %s", abspath)
 
354
            f = self._get_FTP()
 
355
            f.mkd(abspath)
 
356
            self._setmode(relpath, mode)
 
357
        except ftplib.error_perm, e:
 
358
            self._translate_perm_error(e, abspath,
 
359
                unknown_exc=errors.FileExists)
 
360
 
 
361
    def open_write_stream(self, relpath, mode=None):
 
362
        """See Transport.open_write_stream."""
 
363
        self.put_bytes(relpath, "", mode)
 
364
        result = AppendBasedFileStream(self, relpath)
 
365
        _file_streams[self.abspath(relpath)] = result
 
366
        return result
 
367
 
 
368
    def recommended_page_size(self):
 
369
        """See Transport.recommended_page_size().
 
370
 
 
371
        For FTP we suggest a large page size to reduce the overhead
 
372
        introduced by latency.
 
373
        """
 
374
        return 64 * 1024
 
375
 
 
376
    def rmdir(self, rel_path):
 
377
        """Delete the directory at rel_path"""
 
378
        abspath = self._remote_path(rel_path)
 
379
        try:
 
380
            mutter("FTP rmd: %s", abspath)
 
381
            f = self._get_FTP()
 
382
            f.rmd(abspath)
 
383
        except ftplib.error_perm, e:
 
384
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
 
385
 
 
386
    def append_file(self, relpath, f, mode=None):
 
387
        """Append the text in the file-like object into the final
 
388
        location.
 
389
        """
 
390
        abspath = self._remote_path(relpath)
 
391
        if self.has(relpath):
 
392
            ftp = self._get_FTP()
 
393
            result = ftp.size(abspath)
 
394
        else:
 
395
            result = 0
 
396
 
 
397
        mutter("FTP appe to %s", abspath)
 
398
        self._try_append(relpath, f.read(), mode)
 
399
 
 
400
        return result
 
401
 
 
402
    def _try_append(self, relpath, text, mode=None, retries=0):
 
403
        """Try repeatedly to append the given text to the file at relpath.
 
404
        
 
405
        This is a recursive function. On errors, it will be called until the
 
406
        number of retries is exceeded.
 
407
        """
 
408
        try:
 
409
            abspath = self._remote_path(relpath)
 
410
            mutter("FTP appe (try %d) to %s", retries, abspath)
 
411
            ftp = self._get_FTP()
 
412
            ftp.voidcmd("TYPE I")
 
413
            cmd = "APPE %s" % abspath
 
414
            conn = ftp.transfercmd(cmd)
 
415
            conn.sendall(text)
 
416
            conn.close()
 
417
            self._setmode(relpath, mode)
 
418
            ftp.getresp()
 
419
        except ftplib.error_perm, e:
 
420
            self._translate_perm_error(e, abspath, extra='error appending',
 
421
                unknown_exc=errors.NoSuchFile)
 
422
        except ftplib.error_temp, e:
 
423
            if retries > _number_of_retries:
 
424
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
425
                        "Aborting." % abspath, orig_error=e)
 
426
            else:
 
427
                warning("FTP temporary error: %s. Retrying.", str(e))
 
428
                self._reconnect()
 
429
                self._try_append(relpath, text, mode, retries+1)
 
430
 
 
431
    def _setmode(self, relpath, mode):
 
432
        """Set permissions on a path.
 
433
 
 
434
        Only set permissions if the FTP server supports the 'SITE CHMOD'
 
435
        extension.
 
436
        """
 
437
        if mode:
 
438
            try:
 
439
                mutter("FTP site chmod: setting permissions to %s on %s",
 
440
                    str(mode), self._remote_path(relpath))
 
441
                ftp = self._get_FTP()
 
442
                cmd = "SITE CHMOD %s %s" % (oct(mode),
 
443
                                            self._remote_path(relpath))
 
444
                ftp.sendcmd(cmd)
 
445
            except ftplib.error_perm, e:
 
446
                # Command probably not available on this server
 
447
                warning("FTP Could not set permissions to %s on %s. %s",
 
448
                        str(mode), self._remote_path(relpath), str(e))
 
449
 
 
450
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
 
451
    #       to copy something to another machine. And you may be able
 
452
    #       to give it its own address as the 'to' location.
 
453
    #       So implement a fancier 'copy()'
 
454
 
 
455
    def rename(self, rel_from, rel_to):
 
456
        abs_from = self._remote_path(rel_from)
 
457
        abs_to = self._remote_path(rel_to)
 
458
        mutter("FTP rename: %s => %s", abs_from, abs_to)
 
459
        f = self._get_FTP()
 
460
        return self._rename(abs_from, abs_to, f)
 
461
 
 
462
    def _rename(self, abs_from, abs_to, f):
 
463
        try:
 
464
            f.rename(abs_from, abs_to)
 
465
        except ftplib.error_perm, e:
 
466
            self._translate_perm_error(e, abs_from,
 
467
                ': unable to rename to %r' % (abs_to))
 
468
 
 
469
    def move(self, rel_from, rel_to):
 
470
        """Move the item at rel_from to the location at rel_to"""
 
471
        abs_from = self._remote_path(rel_from)
 
472
        abs_to = self._remote_path(rel_to)
 
473
        try:
 
474
            mutter("FTP mv: %s => %s", abs_from, abs_to)
 
475
            f = self._get_FTP()
 
476
            self._rename_and_overwrite(abs_from, abs_to, f)
 
477
        except ftplib.error_perm, e:
 
478
            self._translate_perm_error(e, abs_from,
 
479
                extra='unable to rename to %r' % (rel_to,), 
 
480
                unknown_exc=errors.PathError)
 
481
 
 
482
    def _rename_and_overwrite(self, abs_from, abs_to, f):
 
483
        """Do a fancy rename on the remote server.
 
484
 
 
485
        Using the implementation provided by osutils.
 
486
        """
 
487
        osutils.fancy_rename(abs_from, abs_to,
 
488
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
 
489
            unlink_func=lambda p: self._delete(p, f))
 
490
 
 
491
    def delete(self, relpath):
 
492
        """Delete the item at relpath"""
 
493
        abspath = self._remote_path(relpath)
 
494
        f = self._get_FTP()
 
495
        self._delete(abspath, f)
 
496
 
 
497
    def _delete(self, abspath, f):
 
498
        try:
 
499
            mutter("FTP rm: %s", abspath)
 
500
            f.delete(abspath)
 
501
        except ftplib.error_perm, e:
 
502
            self._translate_perm_error(e, abspath, 'error deleting',
 
503
                unknown_exc=errors.NoSuchFile)
 
504
 
 
505
    def external_url(self):
 
506
        """See bzrlib.transport.Transport.external_url."""
 
507
        # FTP URL's are externally usable.
 
508
        return self.base
 
509
 
 
510
    def listable(self):
 
511
        """See Transport.listable."""
 
512
        return True
 
513
 
 
514
    def list_dir(self, relpath):
 
515
        """See Transport.list_dir."""
 
516
        basepath = self._remote_path(relpath)
 
517
        mutter("FTP nlst: %s", basepath)
 
518
        f = self._get_FTP()
 
519
        try:
 
520
            paths = f.nlst(basepath)
 
521
        except ftplib.error_perm, e:
 
522
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
523
        except ftplib.error_temp, e:
 
524
            # xs4all's ftp server raises a 450 temp error when listing an empty
 
525
            # directory. Check for that and just return an empty list in that
 
526
            # case. See bug #215522
 
527
            if str(e).lower().startswith('450 no files found'):
 
528
                mutter('FTP Server returned "%s" for nlst.'
 
529
                       ' Assuming it means empty directory',
 
530
                       str(e))
 
531
                return []
 
532
            raise
 
533
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
534
        if paths and paths[0].startswith(basepath):
 
535
            entries = [path[len(basepath)+1:] for path in paths]
 
536
        else:
 
537
            entries = paths
 
538
        # Remove . and .. if present
 
539
        return [urlutils.escape(entry) for entry in entries
 
540
                if entry not in ('.', '..')]
 
541
 
 
542
    def iter_files_recursive(self):
 
543
        """See Transport.iter_files_recursive.
 
544
 
 
545
        This is cargo-culted from the SFTP transport"""
 
546
        mutter("FTP iter_files_recursive")
 
547
        queue = list(self.list_dir("."))
 
548
        while queue:
 
549
            relpath = queue.pop(0)
 
550
            st = self.stat(relpath)
 
551
            if stat.S_ISDIR(st.st_mode):
 
552
                for i, basename in enumerate(self.list_dir(relpath)):
 
553
                    queue.insert(i, relpath+"/"+basename)
 
554
            else:
 
555
                yield relpath
 
556
 
 
557
    def stat(self, relpath):
 
558
        """Return the stat information for a file."""
 
559
        abspath = self._remote_path(relpath)
 
560
        try:
 
561
            mutter("FTP stat: %s", abspath)
 
562
            f = self._get_FTP()
 
563
            return FtpStatResult(f, abspath)
 
564
        except ftplib.error_perm, e:
 
565
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
566
 
 
567
    def lock_read(self, relpath):
 
568
        """Lock the given file for shared (read) access.
 
569
        :return: A lock object, which should be passed to Transport.unlock()
 
570
        """
 
571
        # The old RemoteBranch ignore lock for reading, so we will
 
572
        # continue that tradition and return a bogus lock object.
 
573
        class BogusLock(object):
 
574
            def __init__(self, path):
 
575
                self.path = path
 
576
            def unlock(self):
 
577
                pass
 
578
        return BogusLock(relpath)
 
579
 
 
580
    def lock_write(self, relpath):
 
581
        """Lock the given file for exclusive (write) access.
 
582
        WARNING: many transports do not support this, so trying avoid using it
 
583
 
 
584
        :return: A lock object, which should be passed to Transport.unlock()
 
585
        """
 
586
        return self.lock_read(relpath)
 
587
 
 
588
 
 
589
def get_test_permutations():
 
590
    """Return the permutations to be used in testing."""
 
591
    from bzrlib import tests
 
592
    if tests.FTPServerFeature.available():
 
593
        from bzrlib.tests import ftp_server
 
594
        return [(FtpTransport, ftp_server.FTPServer)]
 
595
    else:
 
596
        # Dummy server to have the test suite report the number of tests
 
597
        # needing that feature. We raise UnavailableFeature from methods before
 
598
        # the test server is being used. Doing so in the setUp method has bad
 
599
        # side-effects (tearDown is never called).
 
600
        class UnavailableFTPServer(object):
 
601
 
 
602
            def setUp(self):
 
603
                pass
 
604
 
 
605
            def tearDown(self):
 
606
                pass
 
607
 
 
608
            def get_url(self):
 
609
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
610
 
 
611
            def get_bogus_url(self):
 
612
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
613
 
 
614
        return [(FtpTransport, UnavailableFTPServer)]