~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: 2007-12-18 23:41:30 UTC
  • mfrom: (3099.3.7 graph_optimization)
  • Revision ID: pqm@pqm.ubuntu.com-20071218234130-061grgxsaf1g7bao
(jam) Implement ParentProviders.get_parent_map() and deprecate
        get_parents()

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
16
"""Implementation of Transport over ftp.
18
17
 
19
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
25
24
active, in which case aftp:// will be your friend.
26
25
"""
27
26
 
28
 
from __future__ import absolute_import
29
 
 
30
27
from cStringIO import StringIO
 
28
import errno
31
29
import ftplib
32
30
import getpass
33
31
import os
34
 
import random
 
32
import os.path
 
33
import urlparse
35
34
import socket
36
35
import stat
37
36
import time
 
37
import random
 
38
from warnings import warn
38
39
 
39
40
from bzrlib import (
40
41
    config,
42
43
    osutils,
43
44
    urlutils,
44
45
    )
45
 
from bzrlib.symbol_versioning import (
46
 
    DEPRECATED_PARAMETER,
47
 
    deprecated_in,
48
 
    deprecated_passed,
49
 
    warn,
50
 
    )
51
46
from bzrlib.trace import mutter, warning
52
47
from bzrlib.transport import (
53
48
    AppendBasedFileStream,
56
51
    register_urlparse_netloc_protocol,
57
52
    Server,
58
53
    )
 
54
from bzrlib.transport.local import LocalURLServer
 
55
import bzrlib.ui
59
56
 
60
57
 
61
58
register_urlparse_netloc_protocol('aftp')
66
63
 
67
64
 
68
65
class FtpStatResult(object):
69
 
 
70
 
    def __init__(self, f, abspath):
 
66
    def __init__(self, f, relpath):
71
67
        try:
72
 
            self.st_size = f.size(abspath)
 
68
            self.st_size = f.size(relpath)
73
69
            self.st_mode = stat.S_IFREG
74
70
        except ftplib.error_perm:
75
71
            pwd = f.pwd()
76
72
            try:
77
 
                f.cwd(abspath)
 
73
                f.cwd(relpath)
78
74
                self.st_mode = stat.S_IFDIR
79
75
            finally:
80
76
                f.cwd(pwd)
93
89
 
94
90
    def __init__(self, base, _from_transport=None):
95
91
        """Set the base path where files will be stored."""
96
 
        if not (base.startswith('ftp://') or base.startswith('aftp://')):
97
 
            raise ValueError(base)
 
92
        assert base.startswith('ftp://') or base.startswith('aftp://')
98
93
        super(FtpTransport, self).__init__(base,
99
94
                                           _from_transport=_from_transport)
100
95
        self._unqualified_scheme = 'ftp'
101
 
        if self._parsed_url.scheme == 'aftp':
 
96
        if self._scheme == 'aftp':
102
97
            self.is_active = True
103
98
        else:
104
99
            self.is_active = False
105
100
 
106
 
        # Most modern FTP servers support the APPE command. If ours doesn't, we
107
 
        # (re)set this flag accordingly later.
108
 
        self._has_append = True
109
 
 
110
101
    def _get_FTP(self):
111
102
        """Return the ftplib.FTP instance for this object."""
112
103
        # Ensures that a connection is established
117
108
            self._set_connection(connection, credentials)
118
109
        return connection
119
110
 
120
 
    connection_class = ftplib.FTP
121
 
 
122
111
    def _create_connection(self, credentials=None):
123
112
        """Create a new connection with the provided credentials.
124
113
 
126
115
 
127
116
        :return: The created connection and its associated credentials.
128
117
 
129
 
        The input credentials are only the password as it may have been
130
 
        entered interactively by the user and may be different from the one
131
 
        provided in base url at transport creation time.  The returned
132
 
        credentials are username, password.
 
118
        The credentials are only the password as it may have been entered
 
119
        interactively by the user and may be different from the one provided
 
120
        in base url at transport creation time.
133
121
        """
134
122
        if credentials is None:
135
123
            user, password = self._user, self._password
138
126
 
139
127
        auth = config.AuthenticationConfig()
140
128
        if user is None:
141
 
            user = auth.get_user('ftp', self._host, port=self._port,
142
 
                                 default=getpass.getuser())
 
129
            user = auth.get_user('ftp', self._host, port=self._port)
 
130
            if user is None:
 
131
                # Default to local user
 
132
                user = getpass.getuser()
 
133
 
143
134
        mutter("Constructing FTP instance against %r" %
144
135
               ((self._host, self._port, user, '********',
145
136
                self.is_active),))
146
137
        try:
147
 
            connection = self.connection_class()
 
138
            connection = ftplib.FTP()
148
139
            connection.connect(host=self._host, port=self._port)
149
 
            self._login(connection, auth, user, password)
 
140
            if user and user != 'anonymous' and \
 
141
                    password is None: # '' is a valid password
 
142
                password = auth.get_password('ftp', self._host, user,
 
143
                                             port=self._port)
 
144
            connection.login(user=user, passwd=password)
150
145
            connection.set_pasv(not self.is_active)
151
 
            # binary mode is the default
152
 
            connection.voidcmd('TYPE I')
153
146
        except socket.error, e:
154
147
            raise errors.SocketConnectionError(self._host, self._port,
155
148
                                               msg='Unable to connect to',
159
152
                                        " %s" % str(e), orig_error=e)
160
153
        return connection, (user, password)
161
154
 
162
 
    def _login(self, connection, auth, user, password):
163
 
        # '' is a valid password
164
 
        if user and user != 'anonymous' and password is None:
165
 
            password = auth.get_password('ftp', self._host,
166
 
                                         user, port=self._port)
167
 
        connection.login(user=user, passwd=password)
168
 
 
169
155
    def _reconnect(self):
170
156
        """Create a new connection with the previously used credentials"""
171
157
        credentials = self._get_credentials()
172
158
        connection, credentials = self._create_connection(credentials)
173
159
        self._set_connection(connection, credentials)
174
160
 
175
 
    def disconnect(self):
176
 
        connection = self._get_connection()
177
 
        if connection is not None:
178
 
            connection.close()
179
 
 
180
 
    def _translate_ftp_error(self, err, path, extra=None,
 
161
    def _translate_perm_error(self, err, path, extra=None,
181
162
                              unknown_exc=FtpPathError):
182
 
        """Try to translate an ftplib exception to a bzrlib exception.
 
163
        """Try to translate an ftplib.error_perm exception.
183
164
 
184
165
        :param err: The error to translate into a bzr error
185
166
        :param path: The path which had problems
187
168
        :param unknown_exc: If None, we will just raise the original exception
188
169
                    otherwise we raise unknown_exc(path, extra=extra)
189
170
        """
190
 
        # ftp error numbers are very generic, like "451: Requested action aborted,
191
 
        # local error in processing" so unfortunately we have to match by
192
 
        # strings.
193
171
        s = str(err).lower()
194
172
        if not extra:
195
173
            extra = str(err)
200
178
            or 'no such dir' in s
201
179
            or 'could not create file' in s # vsftpd
202
180
            or 'file doesn\'t exist' in s
203
 
            or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
204
181
            or 'file/directory not found' in s # filezilla server
205
 
            # Microsoft FTP-Service RNFR reply if file not found
206
 
            or (s.startswith('550 ') and 'unable to rename to' in extra)
207
 
            # if containing directory doesn't exist, suggested by
208
 
            # <https://bugs.launchpad.net/bzr/+bug/224373>
209
 
            or (s.startswith('550 ') and "can't find folder" in s)
210
182
            ):
211
183
            raise errors.NoSuchFile(path, extra=extra)
212
 
        elif ('file exists' in s):
 
184
        if ('file exists' in s):
213
185
            raise errors.FileExists(path, extra=extra)
214
 
        elif ('not a directory' in s):
 
186
        if ('not a directory' in s):
215
187
            raise errors.PathError(path, extra=extra)
216
 
        elif 'directory not empty' in s:
217
 
            raise errors.DirectoryNotEmpty(path, extra=extra)
218
188
 
219
189
        mutter('unable to understand error for path: %s: %s', path, err)
220
190
 
221
191
        if unknown_exc:
222
192
            raise unknown_exc(path, extra=extra)
223
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in
 
193
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
224
194
        #       something like TransportError, but this loses the traceback
225
195
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
226
196
        #       to handle. Consider doing something like that here.
227
197
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
228
198
        raise
229
199
 
 
200
    def _remote_path(self, relpath):
 
201
        # XXX: It seems that ftplib does not handle Unicode paths
 
202
        # at the same time, medusa won't handle utf8 paths So if
 
203
        # we .encode(utf8) here (see ConnectedTransport
 
204
        # implementation), then we get a Server failure.  while
 
205
        # if we use str(), we get a UnicodeError, and the test
 
206
        # suite just skips testing UnicodePaths.
 
207
        relative = str(urlutils.unescape(relpath))
 
208
        remote_path = self._combine_paths(self._path, relative)
 
209
        return remote_path
 
210
 
230
211
    def has(self, relpath):
231
212
        """Does the target location exist?"""
232
213
        # FIXME jam 20060516 We *do* ask about directories in the test suite
248
229
            mutter("FTP has not: %s: %s", abspath, e)
249
230
            return False
250
231
 
251
 
    def get(self, relpath, retries=0):
 
232
    def get(self, relpath, decode=False, retries=0):
252
233
        """Get the file at the given relative path.
253
234
 
254
235
        :param relpath: The relative path to the file
258
239
        We're meant to return a file-like object which bzr will
259
240
        then read from. For now we do this via the magic of StringIO
260
241
        """
 
242
        # TODO: decode should be deprecated
261
243
        try:
262
244
            mutter("FTP get: %s", self._remote_path(relpath))
263
245
            f = self._get_FTP()
275
257
            else:
276
258
                warning("FTP temporary error: %s. Retrying.", str(e))
277
259
                self._reconnect()
278
 
                return self.get(relpath, retries+1)
 
260
                return self.get(relpath, decode, retries+1)
279
261
        except EOFError, e:
280
262
            if retries > _number_of_retries:
281
263
                raise errors.TransportError("FTP control connection closed during GET %s."
285
267
                warning("FTP control connection closed. Trying to reopen.")
286
268
                time.sleep(_sleep_between_retries)
287
269
                self._reconnect()
288
 
                return self.get(relpath, retries+1)
 
270
                return self.get(relpath, decode, retries+1)
289
271
 
290
272
    def put_file(self, relpath, fp, mode=None, retries=0):
291
273
        """Copy the file-like or string object into the location.
324
306
            try:
325
307
                f.storbinary('STOR '+tmp_abspath, fp)
326
308
                self._rename_and_overwrite(tmp_abspath, abspath, f)
327
 
                self._setmode(relpath, mode)
328
309
                if bytes is not None:
329
310
                    return len(bytes)
330
311
                else:
331
312
                    return fp.counted_bytes
332
 
            except (ftplib.error_temp, EOFError), e:
333
 
                warning("Failure during ftp PUT of %s: %s. Deleting temporary file."
334
 
                    % (tmp_abspath, e, ))
 
313
            except (ftplib.error_temp,EOFError), e:
 
314
                warning("Failure during ftp PUT. Deleting temporary file.")
335
315
                try:
336
316
                    f.delete(tmp_abspath)
337
317
                except:
340
320
                    raise e
341
321
                raise
342
322
        except ftplib.error_perm, e:
343
 
            self._translate_ftp_error(e, abspath, extra='could not store',
 
323
            self._translate_perm_error(e, abspath, extra='could not store',
344
324
                                       unknown_exc=errors.NoSuchFile)
345
325
        except ftplib.error_temp, e:
346
326
            if retries > _number_of_retries:
347
 
                raise errors.TransportError(
348
 
                    "FTP temporary error during PUT %s: %s. Aborting."
349
 
                    % (self.abspath(relpath), e), orig_error=e)
 
327
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
 
328
                                     % self.abspath(relpath), orig_error=e)
350
329
            else:
351
330
                warning("FTP temporary error: %s. Retrying.", str(e))
352
331
                self._reconnect()
367
346
        try:
368
347
            mutter("FTP mkd: %s", abspath)
369
348
            f = self._get_FTP()
370
 
            try:
371
 
                f.mkd(abspath)
372
 
            except ftplib.error_reply, e:
373
 
                # <https://bugs.launchpad.net/bzr/+bug/224373> Microsoft FTP
374
 
                # server returns "250 Directory created." which is kind of
375
 
                # reasonable, 250 meaning "requested file action OK", but not what
376
 
                # Python's ftplib expects.
377
 
                if e[0][:3] == '250':
378
 
                    pass
379
 
                else:
380
 
                    raise
381
 
            self._setmode(relpath, mode)
 
349
            f.mkd(abspath)
382
350
        except ftplib.error_perm, e:
383
 
            self._translate_ftp_error(e, abspath,
 
351
            self._translate_perm_error(e, abspath,
384
352
                unknown_exc=errors.FileExists)
385
353
 
386
354
    def open_write_stream(self, relpath, mode=None):
406
374
            f = self._get_FTP()
407
375
            f.rmd(abspath)
408
376
        except ftplib.error_perm, e:
409
 
            self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
 
377
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
410
378
 
411
379
    def append_file(self, relpath, f, mode=None):
412
380
        """Append the text in the file-like object into the final
413
381
        location.
414
382
        """
415
 
        text = f.read()
416
383
        abspath = self._remote_path(relpath)
417
384
        if self.has(relpath):
418
385
            ftp = self._get_FTP()
420
387
        else:
421
388
            result = 0
422
389
 
423
 
        if self._has_append:
424
 
            mutter("FTP appe to %s", abspath)
425
 
            self._try_append(relpath, text, mode)
426
 
        else:
427
 
            self._fallback_append(relpath, text, mode)
 
390
        mutter("FTP appe to %s", abspath)
 
391
        self._try_append(relpath, f.read(), mode)
428
392
 
429
393
        return result
430
394
 
431
395
    def _try_append(self, relpath, text, mode=None, retries=0):
432
396
        """Try repeatedly to append the given text to the file at relpath.
433
 
 
 
397
        
434
398
        This is a recursive function. On errors, it will be called until the
435
399
        number of retries is exceeded.
436
400
        """
438
402
            abspath = self._remote_path(relpath)
439
403
            mutter("FTP appe (try %d) to %s", retries, abspath)
440
404
            ftp = self._get_FTP()
 
405
            ftp.voidcmd("TYPE I")
441
406
            cmd = "APPE %s" % abspath
442
407
            conn = ftp.transfercmd(cmd)
443
408
            conn.sendall(text)
444
409
            conn.close()
445
 
            self._setmode(relpath, mode)
 
410
            if mode:
 
411
                self._setmode(relpath, mode)
446
412
            ftp.getresp()
447
413
        except ftplib.error_perm, e:
448
 
            # Check whether the command is not supported (reply code 502)
449
 
            if str(e).startswith('502 '):
450
 
                warning("FTP server does not support file appending natively. "
451
 
                        "Performance may be severely degraded! (%s)", e)
452
 
                self._has_append = False
453
 
                self._fallback_append(relpath, text, mode)
454
 
            else:
455
 
                self._translate_ftp_error(e, abspath, extra='error appending',
456
 
                    unknown_exc=errors.NoSuchFile)
 
414
            self._translate_perm_error(e, abspath, extra='error appending',
 
415
                unknown_exc=errors.NoSuchFile)
457
416
        except ftplib.error_temp, e:
458
417
            if retries > _number_of_retries:
459
 
                raise errors.TransportError(
460
 
                    "FTP temporary error during APPEND %s. Aborting."
461
 
                    % abspath, orig_error=e)
 
418
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
419
                        "Aborting." % abspath, orig_error=e)
462
420
            else:
463
421
                warning("FTP temporary error: %s. Retrying.", str(e))
464
422
                self._reconnect()
465
423
                self._try_append(relpath, text, mode, retries+1)
466
424
 
467
 
    def _fallback_append(self, relpath, text, mode = None):
468
 
        remote = self.get(relpath)
469
 
        remote.seek(0, os.SEEK_END)
470
 
        remote.write(text)
471
 
        remote.seek(0)
472
 
        return self.put_file(relpath, remote, mode)
473
 
 
474
425
    def _setmode(self, relpath, mode):
475
426
        """Set permissions on a path.
476
427
 
477
428
        Only set permissions if the FTP server supports the 'SITE CHMOD'
478
429
        extension.
479
430
        """
480
 
        if mode:
481
 
            try:
482
 
                mutter("FTP site chmod: setting permissions to %s on %s",
483
 
                       oct(mode), self._remote_path(relpath))
484
 
                ftp = self._get_FTP()
485
 
                cmd = "SITE CHMOD %s %s" % (oct(mode),
486
 
                                            self._remote_path(relpath))
487
 
                ftp.sendcmd(cmd)
488
 
            except ftplib.error_perm, e:
489
 
                # Command probably not available on this server
490
 
                warning("FTP Could not set permissions to %s on %s. %s",
491
 
                        oct(mode), self._remote_path(relpath), str(e))
 
431
        try:
 
432
            mutter("FTP site chmod: setting permissions to %s on %s",
 
433
                str(mode), self._remote_path(relpath))
 
434
            ftp = self._get_FTP()
 
435
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
 
436
            ftp.sendcmd(cmd)
 
437
        except ftplib.error_perm, e:
 
438
            # Command probably not available on this server
 
439
            warning("FTP Could not set permissions to %s on %s. %s",
 
440
                    str(mode), self._remote_path(relpath), str(e))
492
441
 
493
442
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
494
443
    #       to copy something to another machine. And you may be able
505
454
    def _rename(self, abs_from, abs_to, f):
506
455
        try:
507
456
            f.rename(abs_from, abs_to)
508
 
        except (ftplib.error_temp, ftplib.error_perm), e:
509
 
            self._translate_ftp_error(e, abs_from,
 
457
        except ftplib.error_perm, e:
 
458
            self._translate_perm_error(e, abs_from,
510
459
                ': unable to rename to %r' % (abs_to))
511
460
 
512
461
    def move(self, rel_from, rel_to):
518
467
            f = self._get_FTP()
519
468
            self._rename_and_overwrite(abs_from, abs_to, f)
520
469
        except ftplib.error_perm, e:
521
 
            self._translate_ftp_error(e, abs_from,
522
 
                extra='unable to rename to %r' % (rel_to,),
 
470
            self._translate_perm_error(e, abs_from,
 
471
                extra='unable to rename to %r' % (rel_to,), 
523
472
                unknown_exc=errors.PathError)
524
473
 
525
474
    def _rename_and_overwrite(self, abs_from, abs_to, f):
542
491
            mutter("FTP rm: %s", abspath)
543
492
            f.delete(abspath)
544
493
        except ftplib.error_perm, e:
545
 
            self._translate_ftp_error(e, abspath, 'error deleting',
 
494
            self._translate_perm_error(e, abspath, 'error deleting',
546
495
                unknown_exc=errors.NoSuchFile)
547
496
 
548
497
    def external_url(self):
560
509
        mutter("FTP nlst: %s", basepath)
561
510
        f = self._get_FTP()
562
511
        try:
563
 
            try:
564
 
                paths = f.nlst(basepath)
565
 
            except ftplib.error_perm, e:
566
 
                self._translate_ftp_error(e, relpath,
567
 
                                           extra='error with list_dir')
568
 
            except ftplib.error_temp, e:
569
 
                # xs4all's ftp server raises a 450 temp error when listing an
570
 
                # empty directory. Check for that and just return an empty list
571
 
                # in that case. See bug #215522
572
 
                if str(e).lower().startswith('450 no files found'):
573
 
                    mutter('FTP Server returned "%s" for nlst.'
574
 
                           ' Assuming it means empty directory',
575
 
                           str(e))
576
 
                    return []
577
 
                raise
578
 
        finally:
579
 
            # Restore binary mode as nlst switch to ascii mode to retrieve file
580
 
            # list
581
 
            f.voidcmd('TYPE I')
582
 
 
 
512
            paths = f.nlst(basepath)
 
513
        except ftplib.error_perm, e:
 
514
            self._translate_perm_error(e, relpath, extra='error with list_dir')
583
515
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
584
516
        if paths and paths[0].startswith(basepath):
585
517
            entries = [path[len(basepath)+1:] for path in paths]
612
544
            f = self._get_FTP()
613
545
            return FtpStatResult(f, abspath)
614
546
        except ftplib.error_perm, e:
615
 
            self._translate_ftp_error(e, abspath, extra='error w/ stat')
 
547
            self._translate_perm_error(e, abspath, extra='error w/ stat')
616
548
 
617
549
    def lock_read(self, relpath):
618
550
        """Lock the given file for shared (read) access.
638
570
 
639
571
def get_test_permutations():
640
572
    """Return the permutations to be used in testing."""
641
 
    from bzrlib.tests import ftp_server
642
 
    return [(FtpTransport, ftp_server.FTPTestServer)]
 
573
    from bzrlib import tests
 
574
    if tests.FTPServerFeature.available():
 
575
        from bzrlib.tests import ftp_server
 
576
        return [(FtpTransport, ftp_server.FTPServer)]
 
577
    else:
 
578
        # Dummy server to have the test suite report the number of tests
 
579
        # needing that feature.
 
580
        class UnavailableFTPServer(object):
 
581
            def setUp(self):
 
582
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
583
 
 
584
        return [(FtpTransport, UnavailableFTPServer)]