~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Danny van Heumen
  • Date: 2010-03-09 21:42:11 UTC
  • mto: (4634.139.5 2.0)
  • mto: This revision was merged to the branch mainline in revision 5160.
  • Revision ID: danny@dannyvanheumen.nl-20100309214211-iqh42x6qcikgd9p3
Reverted now-useless TODO list.

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, 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
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
 
 
17
16
"""Implementation of Transport over ftp.
18
17
 
19
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
26
25
"""
27
26
 
28
27
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
34
import random
33
35
import socket
34
36
import stat
35
37
import time
 
38
from warnings import warn
36
39
 
37
40
from bzrlib import (
38
41
    config,
40
43
    osutils,
41
44
    urlutils,
42
45
    )
43
 
from bzrlib.symbol_versioning import (
44
 
    DEPRECATED_PARAMETER,
45
 
    deprecated_in,
46
 
    deprecated_passed,
47
 
    warn,
48
 
    )
49
46
from bzrlib.trace import mutter, warning
50
47
from bzrlib.transport import (
51
48
    AppendBasedFileStream,
54
51
    register_urlparse_netloc_protocol,
55
52
    Server,
56
53
    )
 
54
from bzrlib.transport.local import LocalURLServer
 
55
import bzrlib.ui
57
56
 
58
57
 
59
58
register_urlparse_netloc_protocol('aftp')
100
99
            self.is_active = True
101
100
        else:
102
101
            self.is_active = False
103
 
 
104
 
        # Most modern FTP servers support the APPE command. If ours doesn't, we
105
 
        # (re)set this flag accordingly later.
 
102
        
 
103
        # Most modern FTP servers support the APPE command. If ours doesn't, we (re)set this flag accordingly later.
106
104
        self._has_append = True
107
105
 
108
106
    def _get_FTP(self):
115
113
            self._set_connection(connection, credentials)
116
114
        return connection
117
115
 
118
 
    connection_class = ftplib.FTP
119
 
 
120
116
    def _create_connection(self, credentials=None):
121
117
        """Create a new connection with the provided credentials.
122
118
 
142
138
               ((self._host, self._port, user, '********',
143
139
                self.is_active),))
144
140
        try:
145
 
            connection = self.connection_class()
 
141
            connection = ftplib.FTP()
146
142
            connection.connect(host=self._host, port=self._port)
147
 
            self._login(connection, auth, user, password)
 
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)
148
148
            connection.set_pasv(not self.is_active)
149
149
            # binary mode is the default
150
150
            connection.voidcmd('TYPE I')
157
157
                                        " %s" % str(e), orig_error=e)
158
158
        return connection, (user, password)
159
159
 
160
 
    def _login(self, connection, auth, user, password):
161
 
        # '' is a valid password
162
 
        if user and user != 'anonymous' and password is None:
163
 
            password = auth.get_password('ftp', self._host,
164
 
                                         user, port=self._port)
165
 
        connection.login(user=user, passwd=password)
166
 
 
167
160
    def _reconnect(self):
168
161
        """Create a new connection with the previously used credentials"""
169
162
        credentials = self._get_credentials()
170
163
        connection, credentials = self._create_connection(credentials)
171
164
        self._set_connection(connection, credentials)
172
165
 
173
 
    def disconnect(self):
174
 
        connection = self._get_connection()
175
 
        if connection is not None:
176
 
            connection.close()
177
 
 
178
 
    def _translate_ftp_error(self, err, path, extra=None,
 
166
    def _translate_perm_error(self, err, path, extra=None,
179
167
                              unknown_exc=FtpPathError):
180
 
        """Try to translate an ftplib exception to a bzrlib exception.
 
168
        """Try to translate an ftplib.error_perm exception.
181
169
 
182
170
        :param err: The error to translate into a bzr error
183
171
        :param path: The path which had problems
185
173
        :param unknown_exc: If None, we will just raise the original exception
186
174
                    otherwise we raise unknown_exc(path, extra=extra)
187
175
        """
188
 
        # ftp error numbers are very generic, like "451: Requested action aborted,
189
 
        # local error in processing" so unfortunately we have to match by
190
 
        # strings.
191
176
        s = str(err).lower()
192
177
        if not extra:
193
178
            extra = str(err)
202
187
            or 'file/directory not found' in s # filezilla server
203
188
            # Microsoft FTP-Service RNFR reply if file not found
204
189
            or (s.startswith('550 ') and 'unable to rename to' in extra)
205
 
            # if containing directory doesn't exist, suggested by
206
 
            # <https://bugs.edge.launchpad.net/bzr/+bug/224373>
207
 
            or (s.startswith('550 ') and "can't find folder" in s)
208
190
            ):
209
191
            raise errors.NoSuchFile(path, extra=extra)
210
 
        elif ('file exists' in s):
 
192
        if ('file exists' in s):
211
193
            raise errors.FileExists(path, extra=extra)
212
 
        elif ('not a directory' in s):
 
194
        if ('not a directory' in s):
213
195
            raise errors.PathError(path, extra=extra)
214
 
        elif 'directory not empty' in s:
215
 
            raise errors.DirectoryNotEmpty(path, extra=extra)
216
196
 
217
197
        mutter('unable to understand error for path: %s: %s', path, err)
218
198
 
225
205
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
226
206
        raise
227
207
 
 
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
 
228
219
    def has(self, relpath):
229
220
        """Does the target location exist?"""
230
221
        # FIXME jam 20060516 We *do* ask about directories in the test suite
246
237
            mutter("FTP has not: %s: %s", abspath, e)
247
238
            return False
248
239
 
249
 
    def get(self, relpath, decode=DEPRECATED_PARAMETER, retries=0):
 
240
    def get(self, relpath, decode=False, retries=0):
250
241
        """Get the file at the given relative path.
251
242
 
252
243
        :param relpath: The relative path to the file
256
247
        We're meant to return a file-like object which bzr will
257
248
        then read from. For now we do this via the magic of StringIO
258
249
        """
259
 
        if deprecated_passed(decode):
260
 
            warn(deprecated_in((2,3,0)) %
261
 
                 '"decode" parameter to FtpTransport.get()',
262
 
                 DeprecationWarning, stacklevel=2)
 
250
        # TODO: decode should be deprecated
263
251
        try:
264
252
            mutter("FTP get: %s", self._remote_path(relpath))
265
253
            f = self._get_FTP()
331
319
                    return len(bytes)
332
320
                else:
333
321
                    return fp.counted_bytes
334
 
            except (ftplib.error_temp, EOFError), e:
335
 
                warning("Failure during ftp PUT of %s: %s. Deleting temporary file."
336
 
                    % (tmp_abspath, e, ))
 
322
            except (ftplib.error_temp,EOFError), e:
 
323
                warning("Failure during ftp PUT. Deleting temporary file.")
337
324
                try:
338
325
                    f.delete(tmp_abspath)
339
326
                except:
342
329
                    raise e
343
330
                raise
344
331
        except ftplib.error_perm, e:
345
 
            self._translate_ftp_error(e, abspath, extra='could not store',
 
332
            self._translate_perm_error(e, abspath, extra='could not store',
346
333
                                       unknown_exc=errors.NoSuchFile)
347
334
        except ftplib.error_temp, e:
348
335
            if retries > _number_of_retries:
349
 
                raise errors.TransportError(
350
 
                    "FTP temporary error during PUT %s: %s. Aborting."
351
 
                    % (self.abspath(relpath), e), orig_error=e)
 
336
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
 
337
                                     % self.abspath(relpath), orig_error=e)
352
338
            else:
353
339
                warning("FTP temporary error: %s. Retrying.", str(e))
354
340
                self._reconnect()
369
355
        try:
370
356
            mutter("FTP mkd: %s", abspath)
371
357
            f = self._get_FTP()
372
 
            try:
373
 
                f.mkd(abspath)
374
 
            except ftplib.error_reply, e:
375
 
                # <https://bugs.launchpad.net/bzr/+bug/224373> Microsoft FTP
376
 
                # server returns "250 Directory created." which is kind of
377
 
                # reasonable, 250 meaning "requested file action OK", but not what
378
 
                # Python's ftplib expects.
379
 
                if e[0][:3] == '250':
380
 
                    pass
381
 
                else:
382
 
                    raise
 
358
            f.mkd(abspath)
383
359
            self._setmode(relpath, mode)
384
360
        except ftplib.error_perm, e:
385
 
            self._translate_ftp_error(e, abspath,
 
361
            self._translate_perm_error(e, abspath,
386
362
                unknown_exc=errors.FileExists)
387
363
 
388
364
    def open_write_stream(self, relpath, mode=None):
408
384
            f = self._get_FTP()
409
385
            f.rmd(abspath)
410
386
        except ftplib.error_perm, e:
411
 
            self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
 
387
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
412
388
 
413
389
    def append_file(self, relpath, f, mode=None):
414
390
        """Append the text in the file-like object into the final
415
391
        location.
416
392
        """
417
393
        text = f.read()
 
394
        
418
395
        abspath = self._remote_path(relpath)
419
396
        if self.has(relpath):
420
397
            ftp = self._get_FTP()
449
426
        except ftplib.error_perm, e:
450
427
            # Check whether the command is not supported (reply code 502)
451
428
            if str(e).startswith('502 '):
452
 
                warning("FTP server does not support file appending natively. "
453
 
                        "Performance may be severely degraded! (%s)", e)
 
429
                warning("FTP server does not support file appending natively. " \
 
430
                    "Performance may be severely degraded! (%s)", e)
454
431
                self._has_append = False
455
432
                self._fallback_append(relpath, text, mode)
456
433
            else:
457
 
                self._translate_ftp_error(e, abspath, extra='error appending',
 
434
                self._translate_perm_error(e, abspath, extra='error appending',
458
435
                    unknown_exc=errors.NoSuchFile)
 
436
            
459
437
        except ftplib.error_temp, e:
460
438
            if retries > _number_of_retries:
461
 
                raise errors.TransportError(
462
 
                    "FTP temporary error during APPEND %s. Aborting."
463
 
                    % abspath, orig_error=e)
 
439
                raise errors.TransportError("FTP temporary error during APPEND %s." \
 
440
                        "Aborting." % abspath, orig_error=e)
464
441
            else:
465
442
                warning("FTP temporary error: %s. Retrying.", str(e))
466
443
                self._reconnect()
468
445
 
469
446
    def _fallback_append(self, relpath, text, mode = None):
470
447
        remote = self.get(relpath)
471
 
        remote.seek(0, os.SEEK_END)
 
448
        remote.seek(0, 2)
472
449
        remote.write(text)
473
 
        remote.seek(0)
 
450
        remote.seek(0, 0)
474
451
        return self.put_file(relpath, remote, mode)
475
452
 
476
453
    def _setmode(self, relpath, mode):
507
484
    def _rename(self, abs_from, abs_to, f):
508
485
        try:
509
486
            f.rename(abs_from, abs_to)
510
 
        except (ftplib.error_temp, ftplib.error_perm), e:
511
 
            self._translate_ftp_error(e, abs_from,
 
487
        except ftplib.error_perm, e:
 
488
            self._translate_perm_error(e, abs_from,
512
489
                ': unable to rename to %r' % (abs_to))
513
490
 
514
491
    def move(self, rel_from, rel_to):
520
497
            f = self._get_FTP()
521
498
            self._rename_and_overwrite(abs_from, abs_to, f)
522
499
        except ftplib.error_perm, e:
523
 
            self._translate_ftp_error(e, abs_from,
 
500
            self._translate_perm_error(e, abs_from,
524
501
                extra='unable to rename to %r' % (rel_to,),
525
502
                unknown_exc=errors.PathError)
526
503
 
544
521
            mutter("FTP rm: %s", abspath)
545
522
            f.delete(abspath)
546
523
        except ftplib.error_perm, e:
547
 
            self._translate_ftp_error(e, abspath, 'error deleting',
 
524
            self._translate_perm_error(e, abspath, 'error deleting',
548
525
                unknown_exc=errors.NoSuchFile)
549
526
 
550
527
    def external_url(self):
565
542
            try:
566
543
                paths = f.nlst(basepath)
567
544
            except ftplib.error_perm, e:
568
 
                self._translate_ftp_error(e, relpath,
 
545
                self._translate_perm_error(e, relpath,
569
546
                                           extra='error with list_dir')
570
547
            except ftplib.error_temp, e:
571
548
                # xs4all's ftp server raises a 450 temp error when listing an
614
591
            f = self._get_FTP()
615
592
            return FtpStatResult(f, abspath)
616
593
        except ftplib.error_perm, e:
617
 
            self._translate_ftp_error(e, abspath, extra='error w/ stat')
 
594
            self._translate_perm_error(e, abspath, extra='error w/ stat')
618
595
 
619
596
    def lock_read(self, relpath):
620
597
        """Lock the given file for shared (read) access.