~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-06-20 01:09:18 UTC
  • mfrom: (3505.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080620010918-64z4xylh1ap5hgyf
Accept user names with @s in URLs (Neil Martinsen-Burrell)

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
        except ftplib.error_temp, e:
 
520
            # xs4all's ftp server raises a 450 temp error when listing an empty
 
521
            # directory. Check for that and just return an empty list in that
 
522
            # case. See bug #215522
 
523
            if str(e).lower().startswith('450 no files found'):
 
524
                mutter('FTP Server returned "%s" for nlst.'
 
525
                       ' Assuming it means empty directory',
 
526
                       str(e))
 
527
                return []
 
528
            raise
 
529
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
530
        if paths and paths[0].startswith(basepath):
 
531
            entries = [path[len(basepath)+1:] for path in paths]
 
532
        else:
 
533
            entries = paths
 
534
        # Remove . and .. if present
 
535
        return [urlutils.escape(entry) for entry in entries
 
536
                if entry not in ('.', '..')]
 
537
 
 
538
    def iter_files_recursive(self):
 
539
        """See Transport.iter_files_recursive.
 
540
 
 
541
        This is cargo-culted from the SFTP transport"""
 
542
        mutter("FTP iter_files_recursive")
 
543
        queue = list(self.list_dir("."))
 
544
        while queue:
 
545
            relpath = queue.pop(0)
 
546
            st = self.stat(relpath)
 
547
            if stat.S_ISDIR(st.st_mode):
 
548
                for i, basename in enumerate(self.list_dir(relpath)):
 
549
                    queue.insert(i, relpath+"/"+basename)
 
550
            else:
 
551
                yield relpath
 
552
 
 
553
    def stat(self, relpath):
 
554
        """Return the stat information for a file."""
 
555
        abspath = self._remote_path(relpath)
 
556
        try:
 
557
            mutter("FTP stat: %s", abspath)
 
558
            f = self._get_FTP()
 
559
            return FtpStatResult(f, abspath)
 
560
        except ftplib.error_perm, e:
 
561
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
562
 
 
563
    def lock_read(self, relpath):
 
564
        """Lock the given file for shared (read) access.
 
565
        :return: A lock object, which should be passed to Transport.unlock()
 
566
        """
 
567
        # The old RemoteBranch ignore lock for reading, so we will
 
568
        # continue that tradition and return a bogus lock object.
 
569
        class BogusLock(object):
 
570
            def __init__(self, path):
 
571
                self.path = path
 
572
            def unlock(self):
 
573
                pass
 
574
        return BogusLock(relpath)
 
575
 
 
576
    def lock_write(self, relpath):
 
577
        """Lock the given file for exclusive (write) access.
 
578
        WARNING: many transports do not support this, so trying avoid using it
 
579
 
 
580
        :return: A lock object, which should be passed to Transport.unlock()
 
581
        """
 
582
        return self.lock_read(relpath)
 
583
 
 
584
 
 
585
def get_test_permutations():
 
586
    """Return the permutations to be used in testing."""
 
587
    from bzrlib import tests
 
588
    if tests.FTPServerFeature.available():
 
589
        from bzrlib.tests import ftp_server
 
590
        return [(FtpTransport, ftp_server.FTPServer)]
 
591
    else:
 
592
        # Dummy server to have the test suite report the number of tests
 
593
        # needing that feature. We raise UnavailableFeature from methods before
 
594
        # the test server is being used. Doing so in the setUp method has bad
 
595
        # side-effects (tearDown is never called).
 
596
        class UnavailableFTPServer(object):
 
597
 
 
598
            def setUp(self):
 
599
                pass
 
600
 
 
601
            def tearDown(self):
 
602
                pass
 
603
 
 
604
            def get_url(self):
 
605
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
606
 
 
607
            def get_bogus_url(self):
 
608
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
609
 
 
610
        return [(FtpTransport, UnavailableFTPServer)]