~jelmer/ubuntu/maverick/bzr/2.2.5

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

ImportĀ upstreamĀ versionĀ 1.13~rc1

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 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
 
        try:
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)
151
 
        except ftplib.error_perm, e:
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):
164
 
        """Try to translate an ftplib.error_perm exception.
165
 
 
166
 
        :param err: The error to translate into a bzr error
167
 
        :param path: The path which had problems
168
 
        :param extra: Extra information which can be included
169
 
        :param unknown_exc: If None, we will just raise the original exception
170
 
                    otherwise we raise unknown_exc(path, extra=extra)
171
 
        """
172
 
        s = str(err).lower()
173
 
        if not extra:
174
 
            extra = str(err)
175
 
        else:
176
 
            extra += ': ' + str(err)
177
 
        if ('no such file' in s
178
 
            or 'could not open' in s
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)
186
 
            ):
187
 
            raise errors.NoSuchFile(path, extra=extra)
188
 
        if ('file exists' in s):
189
 
            raise errors.FileExists(path, extra=extra)
190
 
        if ('not a directory' in s):
191
 
            raise errors.PathError(path, extra=extra)
192
 
 
193
 
        mutter('unable to understand error for path: %s: %s', path, err)
194
 
 
195
 
        if unknown_exc:
196
 
            raise unknown_exc(path, extra=extra)
197
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
198
 
        #       something like TransportError, but this loses the traceback
199
 
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
200
 
        #       to handle. Consider doing something like that here.
201
 
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
202
 
        raise
203
 
 
204
 
    def _remote_path(self, relpath):
205
 
        # XXX: It seems that ftplib does not handle Unicode paths
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
214
 
 
215
 
    def has(self, relpath):
216
 
        """Does the target location exist?"""
217
 
        # FIXME jam 20060516 We *do* ask about directories in the test suite
218
 
        #       We don't seem to in the actual codebase
219
 
        # XXX: I assume we're never asked has(dirname) and thus I use
220
 
        # the FTP size command and assume that if it doesn't raise,
221
 
        # all is good.
222
 
        abspath = self._remote_path(relpath)
223
 
        try:
224
 
            f = self._get_FTP()
225
 
            mutter('FTP has check: %s => %s', relpath, abspath)
226
 
            s = f.size(abspath)
227
 
            mutter("FTP has: %s", abspath)
228
 
            return True
229
 
        except ftplib.error_perm, e:
230
 
            if ('is a directory' in str(e).lower()):
231
 
                mutter("FTP has dir: %s: %s", abspath, e)
232
 
                return True
233
 
            mutter("FTP has not: %s: %s", abspath, e)
234
 
            return False
235
 
 
236
 
    def get(self, relpath, decode=False, retries=0):
237
 
        """Get the file at the given relative path.
238
 
 
239
 
        :param relpath: The relative path to the file
240
 
        :param retries: Number of retries after temporary failures so far
241
 
                        for this operation.
242
 
 
243
 
        We're meant to return a file-like object which bzr will
244
 
        then read from. For now we do this via the magic of StringIO
245
 
        """
246
 
        # TODO: decode should be deprecated
247
 
        try:
248
 
            mutter("FTP get: %s", self._remote_path(relpath))
249
 
            f = self._get_FTP()
250
 
            ret = StringIO()
251
 
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
252
 
            ret.seek(0)
253
 
            return ret
254
 
        except ftplib.error_perm, e:
255
 
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
256
 
        except ftplib.error_temp, e:
257
 
            if retries > _number_of_retries:
258
 
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
259
 
                                     % self.abspath(relpath),
260
 
                                     orig_error=e)
261
 
            else:
262
 
                warning("FTP temporary error: %s. Retrying.", str(e))
263
 
                self._reconnect()
264
 
                return self.get(relpath, decode, retries+1)
265
 
        except EOFError, e:
266
 
            if retries > _number_of_retries:
267
 
                raise errors.TransportError("FTP control connection closed during GET %s."
268
 
                                     % self.abspath(relpath),
269
 
                                     orig_error=e)
270
 
            else:
271
 
                warning("FTP control connection closed. Trying to reopen.")
272
 
                time.sleep(_sleep_between_retries)
273
 
                self._reconnect()
274
 
                return self.get(relpath, decode, retries+1)
275
 
 
276
 
    def put_file(self, relpath, fp, mode=None, retries=0):
277
 
        """Copy the file-like or string object into the location.
278
 
 
279
 
        :param relpath: Location to put the contents, relative to base.
280
 
        :param fp:       File-like or string object.
281
 
        :param retries: Number of retries after temporary failures so far
282
 
                        for this operation.
283
 
 
284
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
285
 
        ftplib does not
286
 
        """
287
 
        abspath = self._remote_path(relpath)
288
 
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
289
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
290
 
        bytes = None
291
 
        if getattr(fp, 'read', None) is None:
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)
307
 
        try:
308
 
            mutter("FTP put: %s", abspath)
309
 
            f = self._get_FTP()
310
 
            try:
311
 
                f.storbinary('STOR '+tmp_abspath, fp)
312
 
                self._rename_and_overwrite(tmp_abspath, abspath, f)
313
 
                if bytes is not None:
314
 
                    return len(bytes)
315
 
                else:
316
 
                    return fp.counted_bytes
317
 
            except (ftplib.error_temp,EOFError), e:
318
 
                warning("Failure during ftp PUT. Deleting temporary file.")
319
 
                try:
320
 
                    f.delete(tmp_abspath)
321
 
                except:
322
 
                    warning("Failed to delete temporary file on the"
323
 
                            " server.\nFile: %s", tmp_abspath)
324
 
                    raise e
325
 
                raise
326
 
        except ftplib.error_perm, e:
327
 
            self._translate_perm_error(e, abspath, extra='could not store',
328
 
                                       unknown_exc=errors.NoSuchFile)
329
 
        except ftplib.error_temp, e:
330
 
            if retries > _number_of_retries:
331
 
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
332
 
                                     % self.abspath(relpath), orig_error=e)
333
 
            else:
334
 
                warning("FTP temporary error: %s. Retrying.", str(e))
335
 
                self._reconnect()
336
 
                self.put_file(relpath, fp, mode, retries+1)
337
 
        except EOFError:
338
 
            if retries > _number_of_retries:
339
 
                raise errors.TransportError("FTP control connection closed during PUT %s."
340
 
                                     % self.abspath(relpath), orig_error=e)
341
 
            else:
342
 
                warning("FTP control connection closed. Trying to reopen.")
343
 
                time.sleep(_sleep_between_retries)
344
 
                self._reconnect()
345
 
                self.put_file(relpath, fp, mode, retries+1)
346
 
 
347
 
    def mkdir(self, relpath, mode=None):
348
 
        """Create a directory at the given path."""
349
 
        abspath = self._remote_path(relpath)
350
 
        try:
351
 
            mutter("FTP mkd: %s", abspath)
352
 
            f = self._get_FTP()
353
 
            f.mkd(abspath)
354
 
        except ftplib.error_perm, e:
355
 
            self._translate_perm_error(e, abspath,
356
 
                unknown_exc=errors.FileExists)
357
 
 
358
 
    def open_write_stream(self, relpath, mode=None):
359
 
        """See Transport.open_write_stream."""
360
 
        self.put_bytes(relpath, "", mode)
361
 
        result = AppendBasedFileStream(self, relpath)
362
 
        _file_streams[self.abspath(relpath)] = result
363
 
        return result
364
 
 
365
 
    def recommended_page_size(self):
366
 
        """See Transport.recommended_page_size().
367
 
 
368
 
        For FTP we suggest a large page size to reduce the overhead
369
 
        introduced by latency.
370
 
        """
371
 
        return 64 * 1024
372
 
 
373
 
    def rmdir(self, rel_path):
374
 
        """Delete the directory at rel_path"""
375
 
        abspath = self._remote_path(rel_path)
376
 
        try:
377
 
            mutter("FTP rmd: %s", abspath)
378
 
            f = self._get_FTP()
379
 
            f.rmd(abspath)
380
 
        except ftplib.error_perm, e:
381
 
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
382
 
 
383
 
    def append_file(self, relpath, f, mode=None):
384
 
        """Append the text in the file-like object into the final
385
 
        location.
386
 
        """
387
 
        abspath = self._remote_path(relpath)
388
 
        if self.has(relpath):
389
 
            ftp = self._get_FTP()
390
 
            result = ftp.size(abspath)
391
 
        else:
392
 
            result = 0
393
 
 
394
 
        mutter("FTP appe to %s", abspath)
395
 
        self._try_append(relpath, f.read(), mode)
396
 
 
397
 
        return result
398
 
 
399
 
    def _try_append(self, relpath, text, mode=None, retries=0):
400
 
        """Try repeatedly to append the given text to the file at relpath.
401
 
        
402
 
        This is a recursive function. On errors, it will be called until the
403
 
        number of retries is exceeded.
404
 
        """
405
 
        try:
406
 
            abspath = self._remote_path(relpath)
407
 
            mutter("FTP appe (try %d) to %s", retries, abspath)
408
 
            ftp = self._get_FTP()
409
 
            ftp.voidcmd("TYPE I")
410
 
            cmd = "APPE %s" % abspath
411
 
            conn = ftp.transfercmd(cmd)
412
 
            conn.sendall(text)
413
 
            conn.close()
414
 
            if mode:
415
 
                self._setmode(relpath, mode)
416
 
            ftp.getresp()
417
 
        except ftplib.error_perm, e:
418
 
            self._translate_perm_error(e, abspath, extra='error appending',
419
 
                unknown_exc=errors.NoSuchFile)
420
 
        except ftplib.error_temp, e:
421
 
            if retries > _number_of_retries:
422
 
                raise errors.TransportError("FTP temporary error during APPEND %s." \
423
 
                        "Aborting." % abspath, orig_error=e)
424
 
            else:
425
 
                warning("FTP temporary error: %s. Retrying.", str(e))
426
 
                self._reconnect()
427
 
                self._try_append(relpath, text, mode, retries+1)
428
 
 
429
 
    def _setmode(self, relpath, mode):
430
 
        """Set permissions on a path.
431
 
 
432
 
        Only set permissions if the FTP server supports the 'SITE CHMOD'
433
 
        extension.
434
 
        """
435
 
        try:
436
 
            mutter("FTP site chmod: setting permissions to %s on %s",
437
 
                str(mode), self._remote_path(relpath))
438
 
            ftp = self._get_FTP()
439
 
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
440
 
            ftp.sendcmd(cmd)
441
 
        except ftplib.error_perm, e:
442
 
            # Command probably not available on this server
443
 
            warning("FTP Could not set permissions to %s on %s. %s",
444
 
                    str(mode), self._remote_path(relpath), str(e))
445
 
 
446
 
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
447
 
    #       to copy something to another machine. And you may be able
448
 
    #       to give it its own address as the 'to' location.
449
 
    #       So implement a fancier 'copy()'
450
 
 
451
 
    def rename(self, rel_from, rel_to):
452
 
        abs_from = self._remote_path(rel_from)
453
 
        abs_to = self._remote_path(rel_to)
454
 
        mutter("FTP rename: %s => %s", abs_from, abs_to)
455
 
        f = self._get_FTP()
456
 
        return self._rename(abs_from, abs_to, f)
457
 
 
458
 
    def _rename(self, abs_from, abs_to, f):
459
 
        try:
460
 
            f.rename(abs_from, abs_to)
461
 
        except ftplib.error_perm, e:
462
 
            self._translate_perm_error(e, abs_from,
463
 
                ': unable to rename to %r' % (abs_to))
464
 
 
465
 
    def move(self, rel_from, rel_to):
466
 
        """Move the item at rel_from to the location at rel_to"""
467
 
        abs_from = self._remote_path(rel_from)
468
 
        abs_to = self._remote_path(rel_to)
469
 
        try:
470
 
            mutter("FTP mv: %s => %s", abs_from, abs_to)
471
 
            f = self._get_FTP()
472
 
            self._rename_and_overwrite(abs_from, abs_to, f)
473
 
        except ftplib.error_perm, e:
474
 
            self._translate_perm_error(e, abs_from,
475
 
                extra='unable to rename to %r' % (rel_to,), 
476
 
                unknown_exc=errors.PathError)
477
 
 
478
 
    def _rename_and_overwrite(self, abs_from, abs_to, f):
479
 
        """Do a fancy rename on the remote server.
480
 
 
481
 
        Using the implementation provided by osutils.
482
 
        """
483
 
        osutils.fancy_rename(abs_from, abs_to,
484
 
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
485
 
            unlink_func=lambda p: self._delete(p, f))
486
 
 
487
 
    def delete(self, relpath):
488
 
        """Delete the item at relpath"""
489
 
        abspath = self._remote_path(relpath)
490
 
        f = self._get_FTP()
491
 
        self._delete(abspath, f)
492
 
 
493
 
    def _delete(self, abspath, f):
494
 
        try:
495
 
            mutter("FTP rm: %s", abspath)
496
 
            f.delete(abspath)
497
 
        except ftplib.error_perm, e:
498
 
            self._translate_perm_error(e, abspath, 'error deleting',
499
 
                unknown_exc=errors.NoSuchFile)
500
 
 
501
 
    def external_url(self):
502
 
        """See bzrlib.transport.Transport.external_url."""
503
 
        # FTP URL's are externally usable.
504
 
        return self.base
505
 
 
506
 
    def listable(self):
507
 
        """See Transport.listable."""
508
 
        return True
509
 
 
510
 
    def list_dir(self, relpath):
511
 
        """See Transport.list_dir."""
512
 
        basepath = self._remote_path(relpath)
513
 
        mutter("FTP nlst: %s", basepath)
514
 
        f = self._get_FTP()
515
 
        try:
516
 
            paths = f.nlst(basepath)
517
 
        except ftplib.error_perm, e:
518
 
            self._translate_perm_error(e, relpath, extra='error with list_dir')
519
 
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
520
 
        if paths and paths[0].startswith(basepath):
521
 
            entries = [path[len(basepath)+1:] for path in paths]
522
 
        else:
523
 
            entries = paths
524
 
        # Remove . and .. if present
525
 
        return [urlutils.escape(entry) for entry in entries
526
 
                if entry not in ('.', '..')]
527
 
 
528
 
    def iter_files_recursive(self):
529
 
        """See Transport.iter_files_recursive.
530
 
 
531
 
        This is cargo-culted from the SFTP transport"""
532
 
        mutter("FTP iter_files_recursive")
533
 
        queue = list(self.list_dir("."))
534
 
        while queue:
535
 
            relpath = queue.pop(0)
536
 
            st = self.stat(relpath)
537
 
            if stat.S_ISDIR(st.st_mode):
538
 
                for i, basename in enumerate(self.list_dir(relpath)):
539
 
                    queue.insert(i, relpath+"/"+basename)
540
 
            else:
541
 
                yield relpath
542
 
 
543
 
    def stat(self, relpath):
544
 
        """Return the stat information for a file."""
545
 
        abspath = self._remote_path(relpath)
546
 
        try:
547
 
            mutter("FTP stat: %s", abspath)
548
 
            f = self._get_FTP()
549
 
            return FtpStatResult(f, abspath)
550
 
        except ftplib.error_perm, e:
551
 
            self._translate_perm_error(e, abspath, extra='error w/ stat')
552
 
 
553
 
    def lock_read(self, relpath):
554
 
        """Lock the given file for shared (read) access.
555
 
        :return: A lock object, which should be passed to Transport.unlock()
556
 
        """
557
 
        # The old RemoteBranch ignore lock for reading, so we will
558
 
        # continue that tradition and return a bogus lock object.
559
 
        class BogusLock(object):
560
 
            def __init__(self, path):
561
 
                self.path = path
562
 
            def unlock(self):
563
 
                pass
564
 
        return BogusLock(relpath)
565
 
 
566
 
    def lock_write(self, relpath):
567
 
        """Lock the given file for exclusive (write) access.
568
 
        WARNING: many transports do not support this, so trying avoid using it
569
 
 
570
 
        :return: A lock object, which should be passed to Transport.unlock()
571
 
        """
572
 
        return self.lock_read(relpath)
573
 
 
574
 
 
575
 
def get_test_permutations():
576
 
    """Return the permutations to be used in testing."""
577
 
    from bzrlib import tests
578
 
    if tests.FTPServerFeature.available():
579
 
        from bzrlib.tests import ftp_server
580
 
        return [(FtpTransport, ftp_server.FTPServer)]
581
 
    else:
582
 
        # Dummy server to have the test suite report the number of tests
583
 
        # needing that feature. We raise UnavailableFeature from methods before
584
 
        # the test server is being used. Doing so in the setUp method has bad
585
 
        # side-effects (tearDown is never called).
586
 
        class UnavailableFTPServer(object):
587
 
 
588
 
            def setUp(self):
589
 
                pass
590
 
 
591
 
            def tearDown(self):
592
 
                pass
593
 
 
594
 
            def get_url(self):
595
 
                raise tests.UnavailableFeature(tests.FTPServerFeature)
596
 
 
597
 
            def get_bogus_url(self):
598
 
                raise tests.UnavailableFeature(tests.FTPServerFeature)
599
 
 
600
 
        return [(FtpTransport, UnavailableFTPServer)]