/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

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

  • Committer: Andrew Bennetts
  • Date: 2010-04-13 04:33:55 UTC
  • mfrom: (5147 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5149.
  • Revision ID: andrew.bennetts@canonical.com-20100413043355-lg3id0uwtju0k3zs
MergeĀ lp:bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005-2010 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
17
"""Implementation of Transport over ftp.
17
18
 
18
19
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
25
26
"""
26
27
 
27
28
from cStringIO import StringIO
28
 
import errno
29
29
import ftplib
30
30
import getpass
31
31
import os
32
 
import os.path
33
 
import urlparse
 
32
import random
34
33
import socket
35
34
import stat
36
35
import time
37
 
import random
38
 
from warnings import warn
39
36
 
40
37
from bzrlib import (
41
38
    config,
51
48
    register_urlparse_netloc_protocol,
52
49
    Server,
53
50
    )
54
 
from bzrlib.transport.local import LocalURLServer
55
 
import bzrlib.ui
56
51
 
57
52
 
58
53
register_urlparse_netloc_protocol('aftp')
63
58
 
64
59
 
65
60
class FtpStatResult(object):
66
 
    def __init__(self, f, relpath):
 
61
 
 
62
    def __init__(self, f, abspath):
67
63
        try:
68
 
            self.st_size = f.size(relpath)
 
64
            self.st_size = f.size(abspath)
69
65
            self.st_mode = stat.S_IFREG
70
66
        except ftplib.error_perm:
71
67
            pwd = f.pwd()
72
68
            try:
73
 
                f.cwd(relpath)
 
69
                f.cwd(abspath)
74
70
                self.st_mode = stat.S_IFDIR
75
71
            finally:
76
72
                f.cwd(pwd)
99
95
        else:
100
96
            self.is_active = False
101
97
 
 
98
        # Most modern FTP servers support the APPE command. If ours doesn't, we
 
99
        # (re)set this flag accordingly later.
 
100
        self._has_append = True
 
101
 
102
102
    def _get_FTP(self):
103
103
        """Return the ftplib.FTP instance for this object."""
104
104
        # Ensures that a connection is established
109
109
            self._set_connection(connection, credentials)
110
110
        return connection
111
111
 
 
112
    connection_class = ftplib.FTP
 
113
 
112
114
    def _create_connection(self, credentials=None):
113
115
        """Create a new connection with the provided credentials.
114
116
 
116
118
 
117
119
        :return: The created connection and its associated credentials.
118
120
 
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.
 
121
        The input credentials are only the password as it may have been
 
122
        entered interactively by the user and may be different from the one
 
123
        provided in base url at transport creation time.  The returned
 
124
        credentials are username, password.
122
125
        """
123
126
        if credentials is None:
124
127
            user, password = self._user, self._password
127
130
 
128
131
        auth = config.AuthenticationConfig()
129
132
        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
 
 
 
133
            user = auth.get_user('ftp', self._host, port=self._port,
 
134
                                 default=getpass.getuser())
135
135
        mutter("Constructing FTP instance against %r" %
136
136
               ((self._host, self._port, user, '********',
137
137
                self.is_active),))
138
138
        try:
139
 
            connection = ftplib.FTP()
 
139
            connection = self.connection_class()
140
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)
 
141
            self._login(connection, auth, user, password)
146
142
            connection.set_pasv(not self.is_active)
 
143
            # binary mode is the default
 
144
            connection.voidcmd('TYPE I')
147
145
        except socket.error, e:
148
146
            raise errors.SocketConnectionError(self._host, self._port,
149
147
                                               msg='Unable to connect to',
153
151
                                        " %s" % str(e), orig_error=e)
154
152
        return connection, (user, password)
155
153
 
 
154
    def _login(self, connection, auth, user, password):
 
155
        # '' is a valid password
 
156
        if user and user != 'anonymous' and password is None:
 
157
            password = auth.get_password('ftp', self._host,
 
158
                                         user, port=self._port)
 
159
        connection.login(user=user, passwd=password)
 
160
 
156
161
    def _reconnect(self):
157
162
        """Create a new connection with the previously used credentials"""
158
163
        credentials = self._get_credentials()
159
164
        connection, credentials = self._create_connection(credentials)
160
165
        self._set_connection(connection, credentials)
161
166
 
162
 
    def _translate_perm_error(self, err, path, extra=None,
 
167
    def _translate_ftp_error(self, err, path, extra=None,
163
168
                              unknown_exc=FtpPathError):
164
 
        """Try to translate an ftplib.error_perm exception.
 
169
        """Try to translate an ftplib exception to a bzrlib exception.
165
170
 
166
171
        :param err: The error to translate into a bzr error
167
172
        :param path: The path which had problems
169
174
        :param unknown_exc: If None, we will just raise the original exception
170
175
                    otherwise we raise unknown_exc(path, extra=extra)
171
176
        """
 
177
        # ftp error numbers are very generic, like "451: Requested action aborted,
 
178
        # local error in processing" so unfortunately we have to match by
 
179
        # strings.
172
180
        s = str(err).lower()
173
181
        if not extra:
174
182
            extra = str(err)
185
193
            or (s.startswith('550 ') and 'unable to rename to' in extra)
186
194
            ):
187
195
            raise errors.NoSuchFile(path, extra=extra)
188
 
        if ('file exists' in s):
 
196
        elif ('file exists' in s):
189
197
            raise errors.FileExists(path, extra=extra)
190
 
        if ('not a directory' in s):
 
198
        elif ('not a directory' in s):
191
199
            raise errors.PathError(path, extra=extra)
 
200
        elif 'directory not empty' in s:
 
201
            raise errors.DirectoryNotEmpty(path, extra=extra)
192
202
 
193
203
        mutter('unable to understand error for path: %s: %s', path, err)
194
204
 
195
205
        if unknown_exc:
196
206
            raise unknown_exc(path, extra=extra)
197
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
 
207
        # TODO: jam 20060516 Consider re-raising the error wrapped in
198
208
        #       something like TransportError, but this loses the traceback
199
209
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
200
210
        #       to handle. Consider doing something like that here.
201
211
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
202
212
        raise
203
213
 
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
214
    def has(self, relpath):
216
215
        """Does the target location exist?"""
217
216
        # FIXME jam 20060516 We *do* ask about directories in the test suite
325
324
                    raise e
326
325
                raise
327
326
        except ftplib.error_perm, e:
328
 
            self._translate_perm_error(e, abspath, extra='could not store',
 
327
            self._translate_ftp_error(e, abspath, extra='could not store',
329
328
                                       unknown_exc=errors.NoSuchFile)
330
329
        except ftplib.error_temp, e:
331
330
            if retries > _number_of_retries:
354
353
            f.mkd(abspath)
355
354
            self._setmode(relpath, mode)
356
355
        except ftplib.error_perm, e:
357
 
            self._translate_perm_error(e, abspath,
 
356
            self._translate_ftp_error(e, abspath,
358
357
                unknown_exc=errors.FileExists)
359
358
 
360
359
    def open_write_stream(self, relpath, mode=None):
380
379
            f = self._get_FTP()
381
380
            f.rmd(abspath)
382
381
        except ftplib.error_perm, e:
383
 
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
 
382
            self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
384
383
 
385
384
    def append_file(self, relpath, f, mode=None):
386
385
        """Append the text in the file-like object into the final
387
386
        location.
388
387
        """
 
388
        text = f.read()
389
389
        abspath = self._remote_path(relpath)
390
390
        if self.has(relpath):
391
391
            ftp = self._get_FTP()
393
393
        else:
394
394
            result = 0
395
395
 
396
 
        mutter("FTP appe to %s", abspath)
397
 
        self._try_append(relpath, f.read(), mode)
 
396
        if self._has_append:
 
397
            mutter("FTP appe to %s", abspath)
 
398
            self._try_append(relpath, text, mode)
 
399
        else:
 
400
            self._fallback_append(relpath, text, mode)
398
401
 
399
402
        return result
400
403
 
401
404
    def _try_append(self, relpath, text, mode=None, retries=0):
402
405
        """Try repeatedly to append the given text to the file at relpath.
403
 
        
 
406
 
404
407
        This is a recursive function. On errors, it will be called until the
405
408
        number of retries is exceeded.
406
409
        """
408
411
            abspath = self._remote_path(relpath)
409
412
            mutter("FTP appe (try %d) to %s", retries, abspath)
410
413
            ftp = self._get_FTP()
411
 
            ftp.voidcmd("TYPE I")
412
414
            cmd = "APPE %s" % abspath
413
415
            conn = ftp.transfercmd(cmd)
414
416
            conn.sendall(text)
416
418
            self._setmode(relpath, mode)
417
419
            ftp.getresp()
418
420
        except ftplib.error_perm, e:
419
 
            self._translate_perm_error(e, abspath, extra='error appending',
420
 
                unknown_exc=errors.NoSuchFile)
 
421
            # Check whether the command is not supported (reply code 502)
 
422
            if str(e).startswith('502 '):
 
423
                warning("FTP server does not support file appending natively. "
 
424
                        "Performance may be severely degraded! (%s)", e)
 
425
                self._has_append = False
 
426
                self._fallback_append(relpath, text, mode)
 
427
            else:
 
428
                self._translate_ftp_error(e, abspath, extra='error appending',
 
429
                    unknown_exc=errors.NoSuchFile)
421
430
        except ftplib.error_temp, e:
422
431
            if retries > _number_of_retries:
423
 
                raise errors.TransportError("FTP temporary error during APPEND %s." \
424
 
                        "Aborting." % abspath, orig_error=e)
 
432
                raise errors.TransportError(
 
433
                    "FTP temporary error during APPEND %s. Aborting."
 
434
                    % abspath, orig_error=e)
425
435
            else:
426
436
                warning("FTP temporary error: %s. Retrying.", str(e))
427
437
                self._reconnect()
428
438
                self._try_append(relpath, text, mode, retries+1)
429
439
 
 
440
    def _fallback_append(self, relpath, text, mode = None):
 
441
        remote = self.get(relpath)
 
442
        remote.seek(0, os.SEEK_END)
 
443
        remote.write(text)
 
444
        remote.seek(0)
 
445
        return self.put_file(relpath, remote, mode)
 
446
 
430
447
    def _setmode(self, relpath, mode):
431
448
        """Set permissions on a path.
432
449
 
436
453
        if mode:
437
454
            try:
438
455
                mutter("FTP site chmod: setting permissions to %s on %s",
439
 
                    str(mode), self._remote_path(relpath))
 
456
                       oct(mode), self._remote_path(relpath))
440
457
                ftp = self._get_FTP()
441
458
                cmd = "SITE CHMOD %s %s" % (oct(mode),
442
459
                                            self._remote_path(relpath))
444
461
            except ftplib.error_perm, e:
445
462
                # Command probably not available on this server
446
463
                warning("FTP Could not set permissions to %s on %s. %s",
447
 
                        str(mode), self._remote_path(relpath), str(e))
 
464
                        oct(mode), self._remote_path(relpath), str(e))
448
465
 
449
466
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
450
467
    #       to copy something to another machine. And you may be able
461
478
    def _rename(self, abs_from, abs_to, f):
462
479
        try:
463
480
            f.rename(abs_from, abs_to)
464
 
        except ftplib.error_perm, e:
465
 
            self._translate_perm_error(e, abs_from,
 
481
        except (ftplib.error_temp, ftplib.error_perm), e:
 
482
            self._translate_ftp_error(e, abs_from,
466
483
                ': unable to rename to %r' % (abs_to))
467
484
 
468
485
    def move(self, rel_from, rel_to):
474
491
            f = self._get_FTP()
475
492
            self._rename_and_overwrite(abs_from, abs_to, f)
476
493
        except ftplib.error_perm, e:
477
 
            self._translate_perm_error(e, abs_from,
478
 
                extra='unable to rename to %r' % (rel_to,), 
 
494
            self._translate_ftp_error(e, abs_from,
 
495
                extra='unable to rename to %r' % (rel_to,),
479
496
                unknown_exc=errors.PathError)
480
497
 
481
498
    def _rename_and_overwrite(self, abs_from, abs_to, f):
498
515
            mutter("FTP rm: %s", abspath)
499
516
            f.delete(abspath)
500
517
        except ftplib.error_perm, e:
501
 
            self._translate_perm_error(e, abspath, 'error deleting',
 
518
            self._translate_ftp_error(e, abspath, 'error deleting',
502
519
                unknown_exc=errors.NoSuchFile)
503
520
 
504
521
    def external_url(self):
516
533
        mutter("FTP nlst: %s", basepath)
517
534
        f = self._get_FTP()
518
535
        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
 
536
            try:
 
537
                paths = f.nlst(basepath)
 
538
            except ftplib.error_perm, e:
 
539
                self._translate_ftp_error(e, relpath,
 
540
                                           extra='error with list_dir')
 
541
            except ftplib.error_temp, e:
 
542
                # xs4all's ftp server raises a 450 temp error when listing an
 
543
                # empty directory. Check for that and just return an empty list
 
544
                # in that case. See bug #215522
 
545
                if str(e).lower().startswith('450 no files found'):
 
546
                    mutter('FTP Server returned "%s" for nlst.'
 
547
                           ' Assuming it means empty directory',
 
548
                           str(e))
 
549
                    return []
 
550
                raise
 
551
        finally:
 
552
            # Restore binary mode as nlst switch to ascii mode to retrieve file
 
553
            # list
 
554
            f.voidcmd('TYPE I')
 
555
 
532
556
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
533
557
        if paths and paths[0].startswith(basepath):
534
558
            entries = [path[len(basepath)+1:] for path in paths]
561
585
            f = self._get_FTP()
562
586
            return FtpStatResult(f, abspath)
563
587
        except ftplib.error_perm, e:
564
 
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
588
            self._translate_ftp_error(e, abspath, extra='error w/ stat')
565
589
 
566
590
    def lock_read(self, relpath):
567
591
        """Lock the given file for shared (read) access.
587
611
 
588
612
def get_test_permutations():
589
613
    """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)]
 
614
    from bzrlib.tests import ftp_server
 
615
    return [(FtpTransport, ftp_server.FTPTestServer)]