~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-08-14 17:25:43 UTC
  • mfrom: (3620.2.2 rules.disable)
  • Revision ID: pqm@pqm.ubuntu.com-20080814172543-nl22gdcodusa8rt0
(robertc) Disable .bzrrules from being read from the WT

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
                self._setmode(relpath, mode)
 
314
                if bytes is not None:
 
315
                    return len(bytes)
 
316
                else:
 
317
                    return fp.counted_bytes
 
318
            except (ftplib.error_temp,EOFError), e:
 
319
                warning("Failure during ftp PUT. Deleting temporary file.")
 
320
                try:
 
321
                    f.delete(tmp_abspath)
 
322
                except:
 
323
                    warning("Failed to delete temporary file on the"
 
324
                            " server.\nFile: %s", tmp_abspath)
 
325
                    raise e
 
326
                raise
 
327
        except ftplib.error_perm, e:
 
328
            self._translate_perm_error(e, abspath, extra='could not store',
 
329
                                       unknown_exc=errors.NoSuchFile)
 
330
        except ftplib.error_temp, e:
 
331
            if retries > _number_of_retries:
 
332
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
 
333
                                     % self.abspath(relpath), orig_error=e)
 
334
            else:
 
335
                warning("FTP temporary error: %s. Retrying.", str(e))
 
336
                self._reconnect()
 
337
                self.put_file(relpath, fp, mode, retries+1)
 
338
        except EOFError:
 
339
            if retries > _number_of_retries:
 
340
                raise errors.TransportError("FTP control connection closed during PUT %s."
 
341
                                     % self.abspath(relpath), orig_error=e)
 
342
            else:
 
343
                warning("FTP control connection closed. Trying to reopen.")
 
344
                time.sleep(_sleep_between_retries)
 
345
                self._reconnect()
 
346
                self.put_file(relpath, fp, mode, retries+1)
 
347
 
 
348
    def mkdir(self, relpath, mode=None):
 
349
        """Create a directory at the given path."""
 
350
        abspath = self._remote_path(relpath)
 
351
        try:
 
352
            mutter("FTP mkd: %s", abspath)
 
353
            f = self._get_FTP()
 
354
            f.mkd(abspath)
 
355
            self._setmode(relpath, mode)
 
356
        except ftplib.error_perm, e:
 
357
            self._translate_perm_error(e, abspath,
 
358
                unknown_exc=errors.FileExists)
 
359
 
 
360
    def open_write_stream(self, relpath, mode=None):
 
361
        """See Transport.open_write_stream."""
 
362
        self.put_bytes(relpath, "", mode)
 
363
        result = AppendBasedFileStream(self, relpath)
 
364
        _file_streams[self.abspath(relpath)] = result
 
365
        return result
 
366
 
 
367
    def recommended_page_size(self):
 
368
        """See Transport.recommended_page_size().
 
369
 
 
370
        For FTP we suggest a large page size to reduce the overhead
 
371
        introduced by latency.
 
372
        """
 
373
        return 64 * 1024
 
374
 
 
375
    def rmdir(self, rel_path):
 
376
        """Delete the directory at rel_path"""
 
377
        abspath = self._remote_path(rel_path)
 
378
        try:
 
379
            mutter("FTP rmd: %s", abspath)
 
380
            f = self._get_FTP()
 
381
            f.rmd(abspath)
 
382
        except ftplib.error_perm, e:
 
383
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
 
384
 
 
385
    def append_file(self, relpath, f, mode=None):
 
386
        """Append the text in the file-like object into the final
 
387
        location.
 
388
        """
 
389
        abspath = self._remote_path(relpath)
 
390
        if self.has(relpath):
 
391
            ftp = self._get_FTP()
 
392
            result = ftp.size(abspath)
 
393
        else:
 
394
            result = 0
 
395
 
 
396
        mutter("FTP appe to %s", abspath)
 
397
        self._try_append(relpath, f.read(), mode)
 
398
 
 
399
        return result
 
400
 
 
401
    def _try_append(self, relpath, text, mode=None, retries=0):
 
402
        """Try repeatedly to append the given text to the file at relpath.
 
403
        
 
404
        This is a recursive function. On errors, it will be called until the
 
405
        number of retries is exceeded.
 
406
        """
 
407
        try:
 
408
            abspath = self._remote_path(relpath)
 
409
            mutter("FTP appe (try %d) to %s", retries, abspath)
 
410
            ftp = self._get_FTP()
 
411
            ftp.voidcmd("TYPE I")
 
412
            cmd = "APPE %s" % abspath
 
413
            conn = ftp.transfercmd(cmd)
 
414
            conn.sendall(text)
 
415
            conn.close()
 
416
            self._setmode(relpath, mode)
 
417
            ftp.getresp()
 
418
        except ftplib.error_perm, e:
 
419
            self._translate_perm_error(e, abspath, extra='error appending',
 
420
                unknown_exc=errors.NoSuchFile)
 
421
        except ftplib.error_temp, e:
 
422
            if retries > _number_of_retries:
 
423
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
424
                        "Aborting." % abspath, orig_error=e)
 
425
            else:
 
426
                warning("FTP temporary error: %s. Retrying.", str(e))
 
427
                self._reconnect()
 
428
                self._try_append(relpath, text, mode, retries+1)
 
429
 
 
430
    def _setmode(self, relpath, mode):
 
431
        """Set permissions on a path.
 
432
 
 
433
        Only set permissions if the FTP server supports the 'SITE CHMOD'
 
434
        extension.
 
435
        """
 
436
        if mode:
 
437
            try:
 
438
                mutter("FTP site chmod: setting permissions to %s on %s",
 
439
                    str(mode), self._remote_path(relpath))
 
440
                ftp = self._get_FTP()
 
441
                cmd = "SITE CHMOD %s %s" % (oct(mode),
 
442
                                            self._remote_path(relpath))
 
443
                ftp.sendcmd(cmd)
 
444
            except ftplib.error_perm, e:
 
445
                # Command probably not available on this server
 
446
                warning("FTP Could not set permissions to %s on %s. %s",
 
447
                        str(mode), self._remote_path(relpath), str(e))
 
448
 
 
449
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
 
450
    #       to copy something to another machine. And you may be able
 
451
    #       to give it its own address as the 'to' location.
 
452
    #       So implement a fancier 'copy()'
 
453
 
 
454
    def rename(self, rel_from, rel_to):
 
455
        abs_from = self._remote_path(rel_from)
 
456
        abs_to = self._remote_path(rel_to)
 
457
        mutter("FTP rename: %s => %s", abs_from, abs_to)
 
458
        f = self._get_FTP()
 
459
        return self._rename(abs_from, abs_to, f)
 
460
 
 
461
    def _rename(self, abs_from, abs_to, f):
 
462
        try:
 
463
            f.rename(abs_from, abs_to)
 
464
        except ftplib.error_perm, e:
 
465
            self._translate_perm_error(e, abs_from,
 
466
                ': unable to rename to %r' % (abs_to))
 
467
 
 
468
    def move(self, rel_from, rel_to):
 
469
        """Move the item at rel_from to the location at rel_to"""
 
470
        abs_from = self._remote_path(rel_from)
 
471
        abs_to = self._remote_path(rel_to)
 
472
        try:
 
473
            mutter("FTP mv: %s => %s", abs_from, abs_to)
 
474
            f = self._get_FTP()
 
475
            self._rename_and_overwrite(abs_from, abs_to, f)
 
476
        except ftplib.error_perm, e:
 
477
            self._translate_perm_error(e, abs_from,
 
478
                extra='unable to rename to %r' % (rel_to,), 
 
479
                unknown_exc=errors.PathError)
 
480
 
 
481
    def _rename_and_overwrite(self, abs_from, abs_to, f):
 
482
        """Do a fancy rename on the remote server.
 
483
 
 
484
        Using the implementation provided by osutils.
 
485
        """
 
486
        osutils.fancy_rename(abs_from, abs_to,
 
487
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
 
488
            unlink_func=lambda p: self._delete(p, f))
 
489
 
 
490
    def delete(self, relpath):
 
491
        """Delete the item at relpath"""
 
492
        abspath = self._remote_path(relpath)
 
493
        f = self._get_FTP()
 
494
        self._delete(abspath, f)
 
495
 
 
496
    def _delete(self, abspath, f):
 
497
        try:
 
498
            mutter("FTP rm: %s", abspath)
 
499
            f.delete(abspath)
 
500
        except ftplib.error_perm, e:
 
501
            self._translate_perm_error(e, abspath, 'error deleting',
 
502
                unknown_exc=errors.NoSuchFile)
 
503
 
 
504
    def external_url(self):
 
505
        """See bzrlib.transport.Transport.external_url."""
 
506
        # FTP URL's are externally usable.
 
507
        return self.base
 
508
 
 
509
    def listable(self):
 
510
        """See Transport.listable."""
 
511
        return True
 
512
 
 
513
    def list_dir(self, relpath):
 
514
        """See Transport.list_dir."""
 
515
        basepath = self._remote_path(relpath)
 
516
        mutter("FTP nlst: %s", basepath)
 
517
        f = self._get_FTP()
 
518
        try:
 
519
            paths = f.nlst(basepath)
 
520
        except ftplib.error_perm, e:
 
521
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
522
        except ftplib.error_temp, e:
 
523
            # xs4all's ftp server raises a 450 temp error when listing an empty
 
524
            # directory. Check for that and just return an empty list in that
 
525
            # case. See bug #215522
 
526
            if str(e).lower().startswith('450 no files found'):
 
527
                mutter('FTP Server returned "%s" for nlst.'
 
528
                       ' Assuming it means empty directory',
 
529
                       str(e))
 
530
                return []
 
531
            raise
 
532
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
533
        if paths and paths[0].startswith(basepath):
 
534
            entries = [path[len(basepath)+1:] for path in paths]
 
535
        else:
 
536
            entries = paths
 
537
        # Remove . and .. if present
 
538
        return [urlutils.escape(entry) for entry in entries
 
539
                if entry not in ('.', '..')]
 
540
 
 
541
    def iter_files_recursive(self):
 
542
        """See Transport.iter_files_recursive.
 
543
 
 
544
        This is cargo-culted from the SFTP transport"""
 
545
        mutter("FTP iter_files_recursive")
 
546
        queue = list(self.list_dir("."))
 
547
        while queue:
 
548
            relpath = queue.pop(0)
 
549
            st = self.stat(relpath)
 
550
            if stat.S_ISDIR(st.st_mode):
 
551
                for i, basename in enumerate(self.list_dir(relpath)):
 
552
                    queue.insert(i, relpath+"/"+basename)
 
553
            else:
 
554
                yield relpath
 
555
 
 
556
    def stat(self, relpath):
 
557
        """Return the stat information for a file."""
 
558
        abspath = self._remote_path(relpath)
 
559
        try:
 
560
            mutter("FTP stat: %s", abspath)
 
561
            f = self._get_FTP()
 
562
            return FtpStatResult(f, abspath)
 
563
        except ftplib.error_perm, e:
 
564
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
565
 
 
566
    def lock_read(self, relpath):
 
567
        """Lock the given file for shared (read) access.
 
568
        :return: A lock object, which should be passed to Transport.unlock()
 
569
        """
 
570
        # The old RemoteBranch ignore lock for reading, so we will
 
571
        # continue that tradition and return a bogus lock object.
 
572
        class BogusLock(object):
 
573
            def __init__(self, path):
 
574
                self.path = path
 
575
            def unlock(self):
 
576
                pass
 
577
        return BogusLock(relpath)
 
578
 
 
579
    def lock_write(self, relpath):
 
580
        """Lock the given file for exclusive (write) access.
 
581
        WARNING: many transports do not support this, so trying avoid using it
 
582
 
 
583
        :return: A lock object, which should be passed to Transport.unlock()
 
584
        """
 
585
        return self.lock_read(relpath)
 
586
 
 
587
 
 
588
def get_test_permutations():
 
589
    """Return the permutations to be used in testing."""
 
590
    from bzrlib import tests
 
591
    if tests.FTPServerFeature.available():
 
592
        from bzrlib.tests import ftp_server
 
593
        return [(FtpTransport, ftp_server.FTPServer)]
 
594
    else:
 
595
        # Dummy server to have the test suite report the number of tests
 
596
        # needing that feature. We raise UnavailableFeature from methods before
 
597
        # the test server is being used. Doing so in the setUp method has bad
 
598
        # side-effects (tearDown is never called).
 
599
        class UnavailableFTPServer(object):
 
600
 
 
601
            def setUp(self):
 
602
                pass
 
603
 
 
604
            def tearDown(self):
 
605
                pass
 
606
 
 
607
            def get_url(self):
 
608
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
609
 
 
610
            def get_bogus_url(self):
 
611
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
612
 
 
613
        return [(FtpTransport, UnavailableFTPServer)]