~bzr/ubuntu/lucid/bzr/beta-ppa

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp/__init__.py

  • Committer: Martin Pool
  • Date: 2010-07-02 07:29:40 UTC
  • mfrom: (129.1.7 packaging-karmic)
  • Revision ID: mbp@sourcefrog.net-20100702072940-hpzq5elg8wjve8rh
* PPA rebuild.
* PPA rebuild for Karmic.
* PPA rebuild for Jaunty.
* PPA rebuild for Hardy.
* From postinst, actually remove the example bash completion scripts.
  (LP: #249452)
* New upstream release.
* New upstream release.
* New upstream release.
* Revert change to Build-depends: Dapper does not have python-central.
  Should be python-support..
* Target ppa..
* Target ppa..
* Target ppa..
* Target ppa..
* New upstream release.
* Switch to dpkg-source 3.0 (quilt) format.
* Bump standards version to 3.8.4.
* Remove embedded copy of python-configobj. Closes: #555336
* Remove embedded copy of python-elementtree. Closes: #555343
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* debian/control: Fix obsolete-relation-form-in-source
  lintian warning. 
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Split out docs into bzr-doc package.
* New upstream release.
* Added John Francesco Ferlito to Uploaders.
* Fix install path to quick-reference guide
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again, again.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to path changes.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Bump standards version to 3.8.3.
* Remove unused patch system.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix copy and paste tab error in .install file
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
 + Fixes compatibility with Python 2.4. Closes: #537708
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream version.
* Bump standards version to 3.8.2.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add python-pyrex to build-deps to ensure C extensions are always build.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix API compatibility version. (Closes: #526233)
* New upstream release.
  + Fixes default format for upgrade command. (Closes: #464688)
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add missing dependency on zlib development library. (Closes:
  #523595)
* Add zlib build-depends.
* Add zlib build-depends.
* Add zlib build-depends.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Move to section vcs.
* Bump standards version to 3.8.1.
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Recommend ca-certificates. (Closes: #452024)
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Update watch file. bazaar now uses launchpad to host its sources.
* Remove patch for inventory root revision copy, applied upstream.
* New upstream release.
* New upstream release.
* New upstream release
* Force removal of files installed in error to /etc/bash_completion.d/
  (LP: #249452)
* New upstream release.
* New upstream release
* New upstream release.
* Bump standards version.
* Include patch for inventory root revision copy, required for bzr-svn.
* New upstream release.
* Remove unused lintian overrides.
* Correct the package version not to be native.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Final 1.5 release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add myself as a co-maintainer.
* Add a Dm-Upload-Allowed: yes header.
* New upstream bugfix release.
* New upstream release.
* Final 1.3 release.
* New upstream release.
* First release candidate of the upcoming 1.3 release.
* Rebuild to fix the problem caused by a build with a broken python-central.
* New upstream release.
* Rebuild for dapper PPA.
* Apply Lamont's patches to fix build-dependencies on dapper.
  (See: https://bugs.launchpad.net/bzr/+bug/189915)

Show diffs side-by-side

added added

removed removed

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