/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

  • Committer: Martin Pool
  • Date: 2008-04-24 07:38:09 UTC
  • mto: This revision was merged to the branch mainline in revision 3415.
  • Revision ID: mbp@sourcefrog.net-20080424073809-ueh0p57961v1q5cs
Treat assert statements in our code as a hard error

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