/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.py

  • Committer: Martin Pool
  • Date: 2007-09-14 06:31:28 UTC
  • mfrom: (2822 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2823.
  • Revision ID: mbp@sourcefrog.net-20070914063128-0p7mh6zfb4pzdg9p
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
46
46
    )
47
47
from bzrlib.trace import mutter, warning
48
48
from bzrlib.transport import (
 
49
    AppendBasedFileStream,
 
50
    _file_streams,
49
51
    Server,
50
 
    split_url,
51
 
    Transport,
 
52
    ConnectedTransport,
52
53
    )
53
54
from bzrlib.transport.local import LocalURLServer
54
55
import bzrlib.ui
60
61
    """FTP failed for path: %(path)s%(extra)s"""
61
62
 
62
63
 
63
 
_FTP_cache = {}
64
 
def _find_FTP(hostname, port, username, password, is_active):
65
 
    """Find an ftplib.FTP instance attached to this triplet."""
66
 
    key = (hostname, port, username, password, is_active)
67
 
    alt_key = (hostname, port, username, '********', is_active)
68
 
    if key not in _FTP_cache:
69
 
        mutter("Constructing FTP instance against %r" % (alt_key,))
70
 
        conn = ftplib.FTP()
71
 
 
72
 
        conn.connect(host=hostname, port=port)
73
 
        if username and username != 'anonymous' and not password:
74
 
            password = bzrlib.ui.ui_factory.get_password(
75
 
                prompt='FTP %(user)s@%(host)s password',
76
 
                user=username, host=hostname)
77
 
        conn.login(user=username, passwd=password)
78
 
        conn.set_pasv(not is_active)
79
 
 
80
 
        _FTP_cache[key] = conn
81
 
 
82
 
    return _FTP_cache[key]    
83
 
 
84
 
 
85
64
class FtpStatResult(object):
86
65
    def __init__(self, f, relpath):
87
66
        try:
99
78
_number_of_retries = 2
100
79
_sleep_between_retries = 5
101
80
 
102
 
class FtpTransport(Transport):
 
81
# FIXME: there are inconsistencies in the way temporary errors are
 
82
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
 
83
# be taken to analyze the implications for write operations (read operations
 
84
# are safe to retry). Overall even some read operations are never
 
85
# retried. --vila 20070720 (Bug #127164)
 
86
class FtpTransport(ConnectedTransport):
103
87
    """This is the transport agent for ftp:// access."""
104
88
 
105
 
    def __init__(self, base, _provided_instance=None):
 
89
    def __init__(self, base, _from_transport=None):
106
90
        """Set the base path where files will be stored."""
107
91
        assert base.startswith('ftp://') or base.startswith('aftp://')
108
 
 
109
 
        self.is_active = base.startswith('aftp://')
110
 
        if self.is_active:
111
 
            # urlparse won't handle aftp://
112
 
            base = base[1:]
113
 
        if not base.endswith('/'):
114
 
            base += '/'
115
 
        (self._proto, self._username,
116
 
            self._password, self._host,
117
 
            self._port, self._path) = split_url(base)
118
 
        base = self._unparse_url()
119
 
 
120
 
        super(FtpTransport, self).__init__(base)
121
 
        self._FTP_instance = _provided_instance
122
 
 
123
 
    def _unparse_url(self, path=None):
124
 
        if path is None:
125
 
            path = self._path
126
 
        path = urllib.quote(path)
127
 
        netloc = urllib.quote(self._host)
128
 
        if self._username is not None:
129
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
130
 
        if self._port is not None:
131
 
            netloc = '%s:%d' % (netloc, self._port)
132
 
        proto = 'ftp'
133
 
        if self.is_active:
134
 
            proto = 'aftp'
135
 
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
 
92
        super(FtpTransport, self).__init__(base,
 
93
                                           _from_transport=_from_transport)
 
94
        self._unqualified_scheme = 'ftp'
 
95
        if self._scheme == 'aftp':
 
96
            self.is_active = True
 
97
        else:
 
98
            self.is_active = False
136
99
 
137
100
    def _get_FTP(self):
138
101
        """Return the ftplib.FTP instance for this object."""
139
 
        if self._FTP_instance is not None:
140
 
            return self._FTP_instance
141
 
        
 
102
        # Ensures that a connection is established
 
103
        connection = self._get_connection()
 
104
        if connection is None:
 
105
            # First connection ever
 
106
            connection, credentials = self._create_connection()
 
107
            self._set_connection(connection, credentials)
 
108
        return connection
 
109
 
 
110
    def _create_connection(self, credentials=None):
 
111
        """Create a new connection with the provided credentials.
 
112
 
 
113
        :param credentials: The credentials needed to establish the connection.
 
114
 
 
115
        :return: The created connection and its associated credentials.
 
116
 
 
117
        The credentials are only the password as it may have been entered
 
118
        interactively by the user and may be different from the one provided
 
119
        in base url at transport creation time.
 
120
        """
 
121
        if credentials is None:
 
122
            password = self._password
 
123
        else:
 
124
            password = credentials
 
125
 
 
126
        mutter("Constructing FTP instance against %r" %
 
127
               ((self._host, self._port, self._user, '********',
 
128
                self.is_active),))
142
129
        try:
143
 
            self._FTP_instance = _find_FTP(self._host, self._port,
144
 
                                           self._username, self._password,
145
 
                                           self.is_active)
146
 
            return self._FTP_instance
 
130
            connection = ftplib.FTP()
 
131
            connection.connect(host=self._host, port=self._port)
 
132
            if self._user and self._user != 'anonymous' and \
 
133
                    password is None: # '' is a valid password
 
134
                get_password = bzrlib.ui.ui_factory.get_password
 
135
                password = get_password(prompt='FTP %(user)s@%(host)s password',
 
136
                                        user=self._user, host=self._host)
 
137
            connection.login(user=self._user, passwd=password)
 
138
            connection.set_pasv(not self.is_active)
147
139
        except ftplib.error_perm, e:
148
 
            raise errors.TransportError(msg="Error setting up connection: %s"
149
 
                                    % str(e), orig_error=e)
150
 
 
151
 
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
 
140
            raise errors.TransportError(msg="Error setting up connection:"
 
141
                                        " %s" % str(e), orig_error=e)
 
142
        return connection, password
 
143
 
 
144
    def _reconnect(self):
 
145
        """Create a new connection with the previously used credentials"""
 
146
        credentials = self.get_credentials()
 
147
        connection, credentials = self._create_connection(credentials)
 
148
        self._set_connection(connection, credentials)
 
149
 
 
150
    def _translate_perm_error(self, err, path, extra=None,
 
151
                              unknown_exc=FtpPathError):
152
152
        """Try to translate an ftplib.error_perm exception.
153
153
 
154
154
        :param err: The error to translate into a bzr error
185
185
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
186
186
        raise
187
187
 
188
 
    def should_cache(self):
189
 
        """Return True if the data pulled across should be cached locally.
190
 
        """
191
 
        return True
192
 
 
193
 
    def clone(self, offset=None):
194
 
        """Return a new FtpTransport with root at self.base + offset.
195
 
        """
196
 
        mutter("FTP clone")
197
 
        if offset is None:
198
 
            return FtpTransport(self.base, self._FTP_instance)
199
 
        else:
200
 
            return FtpTransport(self.abspath(offset), self._FTP_instance)
201
 
 
202
 
    def _abspath(self, relpath):
203
 
        assert isinstance(relpath, basestring)
204
 
        relpath = urlutils.unescape(relpath)
205
 
        if relpath.startswith('/'):
206
 
            basepath = []
207
 
        else:
208
 
            basepath = self._path.split('/')
209
 
        if len(basepath) > 0 and basepath[-1] == '':
210
 
            basepath = basepath[:-1]
211
 
        for p in relpath.split('/'):
212
 
            if p == '..':
213
 
                if len(basepath) == 0:
214
 
                    # In most filesystems, a request for the parent
215
 
                    # of root, just returns root.
216
 
                    continue
217
 
                basepath.pop()
218
 
            elif p == '.' or p == '':
219
 
                continue # No-op
220
 
            else:
221
 
                basepath.append(p)
222
 
        # Possibly, we could use urlparse.urljoin() here, but
223
 
        # I'm concerned about when it chooses to strip the last
224
 
        # portion of the path, and when it doesn't.
225
 
 
 
188
    def _remote_path(self, relpath):
226
189
        # XXX: It seems that ftplib does not handle Unicode paths
227
 
        # at the same time, medusa won't handle utf8 paths
228
 
        # So if we .encode(utf8) here, then we get a Server failure.
229
 
        # while if we use str(), we get a UnicodeError, and the test suite
230
 
        # just skips testing UnicodePaths.
231
 
        return str('/'.join(basepath) or '/')
232
 
    
233
 
    def abspath(self, relpath):
234
 
        """Return the full url to the given relative path.
235
 
        This can be supplied with a string or a list
236
 
        """
237
 
        path = self._abspath(relpath)
238
 
        return self._unparse_url(path)
 
190
        # at the same time, medusa won't handle utf8 paths So if
 
191
        # we .encode(utf8) here (see ConnectedTransport
 
192
        # implementation), then we get a Server failure.  while
 
193
        # if we use str(), we get a UnicodeError, and the test
 
194
        # suite just skips testing UnicodePaths.
 
195
        relative = str(urlutils.unescape(relpath))
 
196
        remote_path = self._combine_paths(self._path, relative)
 
197
        return remote_path
239
198
 
240
199
    def has(self, relpath):
241
200
        """Does the target location exist?"""
244
203
        # XXX: I assume we're never asked has(dirname) and thus I use
245
204
        # the FTP size command and assume that if it doesn't raise,
246
205
        # all is good.
247
 
        abspath = self._abspath(relpath)
 
206
        abspath = self._remote_path(relpath)
248
207
        try:
249
208
            f = self._get_FTP()
250
209
            mutter('FTP has check: %s => %s', relpath, abspath)
270
229
        """
271
230
        # TODO: decode should be deprecated
272
231
        try:
273
 
            mutter("FTP get: %s", self._abspath(relpath))
 
232
            mutter("FTP get: %s", self._remote_path(relpath))
274
233
            f = self._get_FTP()
275
234
            ret = StringIO()
276
 
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
 
235
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
277
236
            ret.seek(0)
278
237
            return ret
279
238
        except ftplib.error_perm, e:
285
244
                                     orig_error=e)
286
245
            else:
287
246
                warning("FTP temporary error: %s. Retrying.", str(e))
288
 
                self._FTP_instance = None
 
247
                self._reconnect()
289
248
                return self.get(relpath, decode, retries+1)
290
249
        except EOFError, e:
291
250
            if retries > _number_of_retries:
295
254
            else:
296
255
                warning("FTP control connection closed. Trying to reopen.")
297
256
                time.sleep(_sleep_between_retries)
298
 
                self._FTP_instance = None
 
257
                self._reconnect()
299
258
                return self.get(relpath, decode, retries+1)
300
259
 
301
260
    def put_file(self, relpath, fp, mode=None, retries=0):
309
268
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
310
269
        ftplib does not
311
270
        """
312
 
        abspath = self._abspath(relpath)
 
271
        abspath = self._remote_path(relpath)
313
272
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
314
273
                        os.getpid(), random.randint(0,0x7FFFFFFF))
315
274
        if getattr(fp, 'read', None) is None:
338
297
                                     % self.abspath(relpath), orig_error=e)
339
298
            else:
340
299
                warning("FTP temporary error: %s. Retrying.", str(e))
341
 
                self._FTP_instance = None
 
300
                self._reconnect()
342
301
                self.put_file(relpath, fp, mode, retries+1)
343
302
        except EOFError:
344
303
            if retries > _number_of_retries:
347
306
            else:
348
307
                warning("FTP control connection closed. Trying to reopen.")
349
308
                time.sleep(_sleep_between_retries)
350
 
                self._FTP_instance = None
 
309
                self._reconnect()
351
310
                self.put_file(relpath, fp, mode, retries+1)
352
311
 
353
312
    def mkdir(self, relpath, mode=None):
354
313
        """Create a directory at the given path."""
355
 
        abspath = self._abspath(relpath)
 
314
        abspath = self._remote_path(relpath)
356
315
        try:
357
316
            mutter("FTP mkd: %s", abspath)
358
317
            f = self._get_FTP()
361
320
            self._translate_perm_error(e, abspath,
362
321
                unknown_exc=errors.FileExists)
363
322
 
 
323
    def open_write_stream(self, relpath, mode=None):
 
324
        """See Transport.open_write_stream."""
 
325
        self.put_bytes(relpath, "", mode)
 
326
        result = AppendBasedFileStream(self, relpath)
 
327
        _file_streams[self.abspath(relpath)] = result
 
328
        return result
 
329
 
 
330
    def recommended_page_size(self):
 
331
        """See Transport.recommended_page_size().
 
332
 
 
333
        For FTP we suggest a large page size to reduce the overhead
 
334
        introduced by latency.
 
335
        """
 
336
        return 64 * 1024
 
337
 
364
338
    def rmdir(self, rel_path):
365
339
        """Delete the directory at rel_path"""
366
 
        abspath = self._abspath(rel_path)
 
340
        abspath = self._remote_path(rel_path)
367
341
        try:
368
342
            mutter("FTP rmd: %s", abspath)
369
343
            f = self._get_FTP()
375
349
        """Append the text in the file-like object into the final
376
350
        location.
377
351
        """
378
 
        abspath = self._abspath(relpath)
 
352
        abspath = self._remote_path(relpath)
379
353
        if self.has(relpath):
380
354
            ftp = self._get_FTP()
381
355
            result = ftp.size(abspath)
394
368
        number of retries is exceeded.
395
369
        """
396
370
        try:
397
 
            abspath = self._abspath(relpath)
 
371
            abspath = self._remote_path(relpath)
398
372
            mutter("FTP appe (try %d) to %s", retries, abspath)
399
373
            ftp = self._get_FTP()
400
374
            ftp.voidcmd("TYPE I")
414
388
                        "Aborting." % abspath, orig_error=e)
415
389
            else:
416
390
                warning("FTP temporary error: %s. Retrying.", str(e))
417
 
                self._FTP_instance = None
 
391
                self._reconnect()
418
392
                self._try_append(relpath, text, mode, retries+1)
419
393
 
420
394
    def _setmode(self, relpath, mode):
425
399
        """
426
400
        try:
427
401
            mutter("FTP site chmod: setting permissions to %s on %s",
428
 
                str(mode), self._abspath(relpath))
 
402
                str(mode), self._remote_path(relpath))
429
403
            ftp = self._get_FTP()
430
 
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
 
404
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
431
405
            ftp.sendcmd(cmd)
432
406
        except ftplib.error_perm, e:
433
407
            # Command probably not available on this server
434
408
            warning("FTP Could not set permissions to %s on %s. %s",
435
 
                    str(mode), self._abspath(relpath), str(e))
 
409
                    str(mode), self._remote_path(relpath), str(e))
436
410
 
437
411
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
438
412
    #       to copy something to another machine. And you may be able
440
414
    #       So implement a fancier 'copy()'
441
415
 
442
416
    def rename(self, rel_from, rel_to):
443
 
        abs_from = self._abspath(rel_from)
444
 
        abs_to = self._abspath(rel_to)
 
417
        abs_from = self._remote_path(rel_from)
 
418
        abs_to = self._remote_path(rel_to)
445
419
        mutter("FTP rename: %s => %s", abs_from, abs_to)
446
420
        f = self._get_FTP()
447
421
        return self._rename(abs_from, abs_to, f)
455
429
 
456
430
    def move(self, rel_from, rel_to):
457
431
        """Move the item at rel_from to the location at rel_to"""
458
 
        abs_from = self._abspath(rel_from)
459
 
        abs_to = self._abspath(rel_to)
 
432
        abs_from = self._remote_path(rel_from)
 
433
        abs_to = self._remote_path(rel_to)
460
434
        try:
461
435
            mutter("FTP mv: %s => %s", abs_from, abs_to)
462
436
            f = self._get_FTP()
477
451
 
478
452
    def delete(self, relpath):
479
453
        """Delete the item at relpath"""
480
 
        abspath = self._abspath(relpath)
 
454
        abspath = self._remote_path(relpath)
481
455
        f = self._get_FTP()
482
456
        self._delete(abspath, f)
483
457
 
489
463
            self._translate_perm_error(e, abspath, 'error deleting',
490
464
                unknown_exc=errors.NoSuchFile)
491
465
 
 
466
    def external_url(self):
 
467
        """See bzrlib.transport.Transport.external_url."""
 
468
        # FTP URL's are externally usable.
 
469
        return self.base
 
470
 
492
471
    def listable(self):
493
472
        """See Transport.listable."""
494
473
        return True
495
474
 
496
475
    def list_dir(self, relpath):
497
476
        """See Transport.list_dir."""
498
 
        basepath = self._abspath(relpath)
 
477
        basepath = self._remote_path(relpath)
499
478
        mutter("FTP nlst: %s", basepath)
500
479
        f = self._get_FTP()
501
480
        try:
528
507
 
529
508
    def stat(self, relpath):
530
509
        """Return the stat information for a file."""
531
 
        abspath = self._abspath(relpath)
 
510
        abspath = self._remote_path(relpath)
532
511
        try:
533
512
            mutter("FTP stat: %s", abspath)
534
513
            f = self._get_FTP()
598
577
            )
599
578
        self._port = self._ftp_server.getsockname()[1]
600
579
        # Don't let it loop forever, or handle an infinite number of requests.
601
 
        # In this case it will run for 100s, or 1000 requests
 
580
        # In this case it will run for 1000s, or 10000 requests
602
581
        self._async_thread = threading.Thread(
603
582
                target=FtpServer._asyncore_loop_ignore_EBADF,
604
 
                kwargs={'timeout':0.1, 'count':1000})
 
583
                kwargs={'timeout':0.1, 'count':10000})
605
584
        self._async_thread.setDaemon(True)
606
585
        self._async_thread.start()
607
586
 
621
600
        """
622
601
        try:
623
602
            asyncore.loop(*args, **kwargs)
 
603
            # FIXME: If we reach that point, we should raise an exception
 
604
            # explaining that the 'count' parameter in setUp is too low or
 
605
            # testers may wonder why their test just sits there waiting for a
 
606
            # server that is already dead. Note that if the tester waits too
 
607
            # long under pdb the server will also die.
624
608
        except select.error, e:
625
609
            if e.args[0] != errno.EBADF:
626
610
                raise
651
635
 
652
636
        def __init__(self, root):
653
637
            self.root = root
 
638
            # If secured_user is set secured_password will be checked
 
639
            self.secured_user = None
 
640
            self.secured_password = None
654
641
 
655
642
        def authorize(self, channel, username, password):
656
643
            """Return (success, reply_string, filesystem)"""
663
650
            else:
664
651
                channel.read_only = 0
665
652
 
666
 
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
 
653
            # Check secured_user if set
 
654
            if (self.secured_user is not None
 
655
                and username == self.secured_user
 
656
                and password != self.secured_password):
 
657
                return 0, 'Password invalid.', None
 
658
            else:
 
659
                return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
667
660
 
668
661
 
669
662
    class ftp_channel(medusa.ftp_server.ftp_channel):
672
665
        def log(self, message):
673
666
            """Redirect logging requests."""
674
667
            mutter('_ftp_channel: %s', message)
675
 
            
 
668
 
676
669
        def log_info(self, message, type='info'):
677
670
            """Redirect logging requests."""
678
671
            mutter('_ftp_channel %s: %s', type, message)
679
 
            
 
672
 
680
673
        def cmd_rnfr(self, line):
681
674
            """Prepare for renaming a file."""
682
675
            self._renaming = line[1]