~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Andrew Bennetts
  • Date: 2010-01-12 03:53:21 UTC
  • mfrom: (4948 +trunk)
  • mto: This revision was merged to the branch mainline in revision 4964.
  • Revision ID: andrew.bennetts@canonical.com-20100112035321-hofpz5p10224ryj3
Merge lp:bzr, resolving conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007, 2008, 2009 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
"""Implementation of Transport over ftp.
17
17
 
18
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
28
 
import errno
29
28
import ftplib
30
29
import getpass
31
30
import os
32
 
import os.path
33
 
import urlparse
 
31
import random
34
32
import socket
35
33
import stat
36
34
import time
37
 
import random
38
 
from warnings import warn
39
35
 
40
36
from bzrlib import (
41
37
    config,
51
47
    register_urlparse_netloc_protocol,
52
48
    Server,
53
49
    )
54
 
from bzrlib.transport.local import LocalURLServer
55
 
import bzrlib.ui
56
50
 
57
51
 
58
52
register_urlparse_netloc_protocol('aftp')
63
57
 
64
58
 
65
59
class FtpStatResult(object):
66
 
    def __init__(self, f, relpath):
 
60
 
 
61
    def __init__(self, f, abspath):
67
62
        try:
68
 
            self.st_size = f.size(relpath)
 
63
            self.st_size = f.size(abspath)
69
64
            self.st_mode = stat.S_IFREG
70
65
        except ftplib.error_perm:
71
66
            pwd = f.pwd()
72
67
            try:
73
 
                f.cwd(relpath)
 
68
                f.cwd(abspath)
74
69
                self.st_mode = stat.S_IFDIR
75
70
            finally:
76
71
                f.cwd(pwd)
99
94
        else:
100
95
            self.is_active = False
101
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
 
102
101
    def _get_FTP(self):
103
102
        """Return the ftplib.FTP instance for this object."""
104
103
        # Ensures that a connection is established
109
108
            self._set_connection(connection, credentials)
110
109
        return connection
111
110
 
 
111
    connection_class = ftplib.FTP
 
112
 
112
113
    def _create_connection(self, credentials=None):
113
114
        """Create a new connection with the provided credentials.
114
115
 
128
129
 
129
130
        auth = config.AuthenticationConfig()
130
131
        if user is None:
131
 
            user = auth.get_user('ftp', self._host, port=self._port)
132
 
            if user is None:
133
 
                # Default to local user
134
 
                user = getpass.getuser()
135
 
 
 
132
            user = auth.get_user('ftp', self._host, port=self._port,
 
133
                                 default=getpass.getuser())
136
134
        mutter("Constructing FTP instance against %r" %
137
135
               ((self._host, self._port, user, '********',
138
136
                self.is_active),))
139
137
        try:
140
 
            connection = ftplib.FTP()
 
138
            connection = self.connection_class()
141
139
            connection.connect(host=self._host, port=self._port)
142
 
            if user and user != 'anonymous' and \
143
 
                    password is None: # '' is a valid password
144
 
                password = auth.get_password('ftp', self._host, user,
145
 
                                             port=self._port)
146
 
            connection.login(user=user, passwd=password)
 
140
            self._login(connection, auth, user, password)
147
141
            connection.set_pasv(not self.is_active)
 
142
            # binary mode is the default
 
143
            connection.voidcmd('TYPE I')
148
144
        except socket.error, e:
149
145
            raise errors.SocketConnectionError(self._host, self._port,
150
146
                                               msg='Unable to connect to',
154
150
                                        " %s" % str(e), orig_error=e)
155
151
        return connection, (user, password)
156
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
 
157
160
    def _reconnect(self):
158
161
        """Create a new connection with the previously used credentials"""
159
162
        credentials = self._get_credentials()
195
198
 
196
199
        if unknown_exc:
197
200
            raise unknown_exc(path, extra=extra)
198
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
 
201
        # TODO: jam 20060516 Consider re-raising the error wrapped in
199
202
        #       something like TransportError, but this loses the traceback
200
203
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
201
204
        #       to handle. Consider doing something like that here.
202
205
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
203
206
        raise
204
207
 
205
 
    def _remote_path(self, relpath):
206
 
        # XXX: It seems that ftplib does not handle Unicode paths
207
 
        # at the same time, medusa won't handle utf8 paths So if
208
 
        # we .encode(utf8) here (see ConnectedTransport
209
 
        # implementation), then we get a Server failure.  while
210
 
        # if we use str(), we get a UnicodeError, and the test
211
 
        # suite just skips testing UnicodePaths.
212
 
        relative = str(urlutils.unescape(relpath))
213
 
        remote_path = self._combine_paths(self._path, relative)
214
 
        return remote_path
215
 
 
216
208
    def has(self, relpath):
217
209
        """Does the target location exist?"""
218
210
        # FIXME jam 20060516 We *do* ask about directories in the test suite
387
379
        """Append the text in the file-like object into the final
388
380
        location.
389
381
        """
 
382
        text = f.read()
390
383
        abspath = self._remote_path(relpath)
391
384
        if self.has(relpath):
392
385
            ftp = self._get_FTP()
394
387
        else:
395
388
            result = 0
396
389
 
397
 
        mutter("FTP appe to %s", abspath)
398
 
        self._try_append(relpath, f.read(), mode)
 
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)
399
395
 
400
396
        return result
401
397
 
402
398
    def _try_append(self, relpath, text, mode=None, retries=0):
403
399
        """Try repeatedly to append the given text to the file at relpath.
404
 
        
 
400
 
405
401
        This is a recursive function. On errors, it will be called until the
406
402
        number of retries is exceeded.
407
403
        """
409
405
            abspath = self._remote_path(relpath)
410
406
            mutter("FTP appe (try %d) to %s", retries, abspath)
411
407
            ftp = self._get_FTP()
412
 
            ftp.voidcmd("TYPE I")
413
408
            cmd = "APPE %s" % abspath
414
409
            conn = ftp.transfercmd(cmd)
415
410
            conn.sendall(text)
417
412
            self._setmode(relpath, mode)
418
413
            ftp.getresp()
419
414
        except ftplib.error_perm, e:
420
 
            self._translate_perm_error(e, abspath, extra='error appending',
421
 
                unknown_exc=errors.NoSuchFile)
 
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)
422
424
        except ftplib.error_temp, e:
423
425
            if retries > _number_of_retries:
424
 
                raise errors.TransportError("FTP temporary error during APPEND %s." \
425
 
                        "Aborting." % abspath, orig_error=e)
 
426
                raise errors.TransportError(
 
427
                    "FTP temporary error during APPEND %s. Aborting."
 
428
                    % abspath, orig_error=e)
426
429
            else:
427
430
                warning("FTP temporary error: %s. Retrying.", str(e))
428
431
                self._reconnect()
429
432
                self._try_append(relpath, text, mode, retries+1)
430
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
 
431
441
    def _setmode(self, relpath, mode):
432
442
        """Set permissions on a path.
433
443
 
437
447
        if mode:
438
448
            try:
439
449
                mutter("FTP site chmod: setting permissions to %s on %s",
440
 
                    str(mode), self._remote_path(relpath))
 
450
                       oct(mode), self._remote_path(relpath))
441
451
                ftp = self._get_FTP()
442
452
                cmd = "SITE CHMOD %s %s" % (oct(mode),
443
453
                                            self._remote_path(relpath))
445
455
            except ftplib.error_perm, e:
446
456
                # Command probably not available on this server
447
457
                warning("FTP Could not set permissions to %s on %s. %s",
448
 
                        str(mode), self._remote_path(relpath), str(e))
 
458
                        oct(mode), self._remote_path(relpath), str(e))
449
459
 
450
460
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
451
461
    #       to copy something to another machine. And you may be able
476
486
            self._rename_and_overwrite(abs_from, abs_to, f)
477
487
        except ftplib.error_perm, e:
478
488
            self._translate_perm_error(e, abs_from,
479
 
                extra='unable to rename to %r' % (rel_to,), 
 
489
                extra='unable to rename to %r' % (rel_to,),
480
490
                unknown_exc=errors.PathError)
481
491
 
482
492
    def _rename_and_overwrite(self, abs_from, abs_to, f):
517
527
        mutter("FTP nlst: %s", basepath)
518
528
        f = self._get_FTP()
519
529
        try:
520
 
            paths = f.nlst(basepath)
521
 
        except ftplib.error_perm, e:
522
 
            self._translate_perm_error(e, relpath, extra='error with list_dir')
523
 
        except ftplib.error_temp, e:
524
 
            # xs4all's ftp server raises a 450 temp error when listing an empty
525
 
            # directory. Check for that and just return an empty list in that
526
 
            # case. See bug #215522
527
 
            if str(e).lower().startswith('450 no files found'):
528
 
                mutter('FTP Server returned "%s" for nlst.'
529
 
                       ' Assuming it means empty directory',
530
 
                       str(e))
531
 
                return []
532
 
            raise
 
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
 
533
550
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
534
551
        if paths and paths[0].startswith(basepath):
535
552
            entries = [path[len(basepath)+1:] for path in paths]
588
605
 
589
606
def get_test_permutations():
590
607
    """Return the permutations to be used in testing."""
591
 
    from bzrlib import tests
592
 
    if tests.FTPServerFeature.available():
593
 
        from bzrlib.tests import ftp_server
594
 
        return [(FtpTransport, ftp_server.FTPServer)]
595
 
    else:
596
 
        # Dummy server to have the test suite report the number of tests
597
 
        # needing that feature. We raise UnavailableFeature from methods before
598
 
        # the test server is being used. Doing so in the setUp method has bad
599
 
        # side-effects (tearDown is never called).
600
 
        class UnavailableFTPServer(object):
601
 
 
602
 
            def setUp(self, vfs_server=None):
603
 
                pass
604
 
 
605
 
            def tearDown(self):
606
 
                pass
607
 
 
608
 
            def get_url(self):
609
 
                raise tests.UnavailableFeature(tests.FTPServerFeature)
610
 
 
611
 
            def get_bogus_url(self):
612
 
                raise tests.UnavailableFeature(tests.FTPServerFeature)
613
 
 
614
 
        return [(FtpTransport, UnavailableFTPServer)]
 
608
    from bzrlib.tests import ftp_server
 
609
    return [(FtpTransport, ftp_server.FTPTestServer)]