~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-04-07 07:52:50 UTC
  • mfrom: (3340.1.1 208418-1.4)
  • Revision ID: pqm@pqm.ubuntu.com-20080407075250-phs53xnslo8boaeo
Return the correct knit serialisation method in _StreamAccess.
        (Andrew Bennetts, Martin Pool, Robert Collins)

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