~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

Merge up through 2.2.0.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008, 2009 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
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
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
34
32
import random
35
33
import socket
36
34
import stat
37
35
import time
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')
99
94
            self.is_active = True
100
95
        else:
101
96
            self.is_active = False
102
 
        
103
 
        # Most modern FTP servers support the APPE command. If ours doesn't, we (re)set this flag accordingly later.
 
97
 
 
98
        # Most modern FTP servers support the APPE command. If ours doesn't, we
 
99
        # (re)set this flag accordingly later.
104
100
        self._has_append = True
105
101
 
106
102
    def _get_FTP(self):
113
109
            self._set_connection(connection, credentials)
114
110
        return connection
115
111
 
 
112
    connection_class = ftplib.FTP
 
113
 
116
114
    def _create_connection(self, credentials=None):
117
115
        """Create a new connection with the provided credentials.
118
116
 
138
136
               ((self._host, self._port, user, '********',
139
137
                self.is_active),))
140
138
        try:
141
 
            connection = ftplib.FTP()
 
139
            connection = self.connection_class()
142
140
            connection.connect(host=self._host, port=self._port)
143
 
            if user and user != 'anonymous' and \
144
 
                    password is None: # '' is a valid password
145
 
                password = auth.get_password('ftp', self._host, user,
146
 
                                             port=self._port)
147
 
            connection.login(user=user, passwd=password)
 
141
            self._login(connection, auth, user, password)
148
142
            connection.set_pasv(not self.is_active)
149
143
            # binary mode is the default
150
144
            connection.voidcmd('TYPE I')
157
151
                                        " %s" % str(e), orig_error=e)
158
152
        return connection, (user, password)
159
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
 
160
161
    def _reconnect(self):
161
162
        """Create a new connection with the previously used credentials"""
162
163
        credentials = self._get_credentials()
163
164
        connection, credentials = self._create_connection(credentials)
164
165
        self._set_connection(connection, credentials)
165
166
 
166
 
    def _translate_perm_error(self, err, path, extra=None,
 
167
    def _translate_ftp_error(self, err, path, extra=None,
167
168
                              unknown_exc=FtpPathError):
168
 
        """Try to translate an ftplib.error_perm exception.
 
169
        """Try to translate an ftplib exception to a bzrlib exception.
169
170
 
170
171
        :param err: The error to translate into a bzr error
171
172
        :param path: The path which had problems
173
174
        :param unknown_exc: If None, we will just raise the original exception
174
175
                    otherwise we raise unknown_exc(path, extra=extra)
175
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.
176
180
        s = str(err).lower()
177
181
        if not extra:
178
182
            extra = str(err)
189
193
            or (s.startswith('550 ') and 'unable to rename to' in extra)
190
194
            ):
191
195
            raise errors.NoSuchFile(path, extra=extra)
192
 
        if ('file exists' in s):
 
196
        elif ('file exists' in s):
193
197
            raise errors.FileExists(path, extra=extra)
194
 
        if ('not a directory' in s):
 
198
        elif ('not a directory' in s):
195
199
            raise errors.PathError(path, extra=extra)
 
200
        elif 'directory not empty' in s:
 
201
            raise errors.DirectoryNotEmpty(path, extra=extra)
196
202
 
197
203
        mutter('unable to understand error for path: %s: %s', path, err)
198
204
 
205
211
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
206
212
        raise
207
213
 
208
 
    def _remote_path(self, relpath):
209
 
        # XXX: It seems that ftplib does not handle Unicode paths
210
 
        # at the same time, medusa won't handle utf8 paths So if
211
 
        # we .encode(utf8) here (see ConnectedTransport
212
 
        # implementation), then we get a Server failure.  while
213
 
        # if we use str(), we get a UnicodeError, and the test
214
 
        # suite just skips testing UnicodePaths.
215
 
        relative = str(urlutils.unescape(relpath))
216
 
        remote_path = self._combine_paths(self._path, relative)
217
 
        return remote_path
218
 
 
219
214
    def has(self, relpath):
220
215
        """Does the target location exist?"""
221
216
        # FIXME jam 20060516 We *do* ask about directories in the test suite
329
324
                    raise e
330
325
                raise
331
326
        except ftplib.error_perm, e:
332
 
            self._translate_perm_error(e, abspath, extra='could not store',
 
327
            self._translate_ftp_error(e, abspath, extra='could not store',
333
328
                                       unknown_exc=errors.NoSuchFile)
334
329
        except ftplib.error_temp, e:
335
330
            if retries > _number_of_retries:
358
353
            f.mkd(abspath)
359
354
            self._setmode(relpath, mode)
360
355
        except ftplib.error_perm, e:
361
 
            self._translate_perm_error(e, abspath,
 
356
            self._translate_ftp_error(e, abspath,
362
357
                unknown_exc=errors.FileExists)
363
358
 
364
359
    def open_write_stream(self, relpath, mode=None):
384
379
            f = self._get_FTP()
385
380
            f.rmd(abspath)
386
381
        except ftplib.error_perm, e:
387
 
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
 
382
            self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
388
383
 
389
384
    def append_file(self, relpath, f, mode=None):
390
385
        """Append the text in the file-like object into the final
391
386
        location.
392
387
        """
393
388
        text = f.read()
394
 
        
395
389
        abspath = self._remote_path(relpath)
396
390
        if self.has(relpath):
397
391
            ftp = self._get_FTP()
426
420
        except ftplib.error_perm, e:
427
421
            # Check whether the command is not supported (reply code 502)
428
422
            if str(e).startswith('502 '):
429
 
                warning("FTP server does not support file appending natively. " \
430
 
                    "Performance may be severely degraded! (%s)", e)
 
423
                warning("FTP server does not support file appending natively. "
 
424
                        "Performance may be severely degraded! (%s)", e)
431
425
                self._has_append = False
432
426
                self._fallback_append(relpath, text, mode)
433
427
            else:
434
 
                self._translate_perm_error(e, abspath, extra='error appending',
 
428
                self._translate_ftp_error(e, abspath, extra='error appending',
435
429
                    unknown_exc=errors.NoSuchFile)
436
 
            
437
430
        except ftplib.error_temp, e:
438
431
            if retries > _number_of_retries:
439
 
                raise errors.TransportError("FTP temporary error during APPEND %s." \
440
 
                        "Aborting." % abspath, orig_error=e)
 
432
                raise errors.TransportError(
 
433
                    "FTP temporary error during APPEND %s. Aborting."
 
434
                    % abspath, orig_error=e)
441
435
            else:
442
436
                warning("FTP temporary error: %s. Retrying.", str(e))
443
437
                self._reconnect()
445
439
 
446
440
    def _fallback_append(self, relpath, text, mode = None):
447
441
        remote = self.get(relpath)
448
 
        remote.seek(0, 2)
 
442
        remote.seek(0, os.SEEK_END)
449
443
        remote.write(text)
450
 
        remote.seek(0, 0)
 
444
        remote.seek(0)
451
445
        return self.put_file(relpath, remote, mode)
452
446
 
453
447
    def _setmode(self, relpath, mode):
484
478
    def _rename(self, abs_from, abs_to, f):
485
479
        try:
486
480
            f.rename(abs_from, abs_to)
487
 
        except ftplib.error_perm, e:
488
 
            self._translate_perm_error(e, abs_from,
 
481
        except (ftplib.error_temp, ftplib.error_perm), e:
 
482
            self._translate_ftp_error(e, abs_from,
489
483
                ': unable to rename to %r' % (abs_to))
490
484
 
491
485
    def move(self, rel_from, rel_to):
497
491
            f = self._get_FTP()
498
492
            self._rename_and_overwrite(abs_from, abs_to, f)
499
493
        except ftplib.error_perm, e:
500
 
            self._translate_perm_error(e, abs_from,
 
494
            self._translate_ftp_error(e, abs_from,
501
495
                extra='unable to rename to %r' % (rel_to,),
502
496
                unknown_exc=errors.PathError)
503
497
 
521
515
            mutter("FTP rm: %s", abspath)
522
516
            f.delete(abspath)
523
517
        except ftplib.error_perm, e:
524
 
            self._translate_perm_error(e, abspath, 'error deleting',
 
518
            self._translate_ftp_error(e, abspath, 'error deleting',
525
519
                unknown_exc=errors.NoSuchFile)
526
520
 
527
521
    def external_url(self):
542
536
            try:
543
537
                paths = f.nlst(basepath)
544
538
            except ftplib.error_perm, e:
545
 
                self._translate_perm_error(e, relpath,
 
539
                self._translate_ftp_error(e, relpath,
546
540
                                           extra='error with list_dir')
547
541
            except ftplib.error_temp, e:
548
542
                # xs4all's ftp server raises a 450 temp error when listing an
591
585
            f = self._get_FTP()
592
586
            return FtpStatResult(f, abspath)
593
587
        except ftplib.error_perm, e:
594
 
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
588
            self._translate_ftp_error(e, abspath, extra='error w/ stat')
595
589
 
596
590
    def lock_read(self, relpath):
597
591
        """Lock the given file for shared (read) access.