~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Ian Clatworthy
  • Date: 2008-03-27 07:51:10 UTC
  • mto: (3311.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3312.
  • Revision ID: ian.clatworthy@canonical.com-20080327075110-afgd7x03ybju06ez
Reduce evangelism in the User Guide

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
24
24
 
25
25
from cStringIO import StringIO
 
26
import urllib
 
27
import urlparse
26
28
 
27
29
from bzrlib import (
28
30
    config,
29
31
    debug,
30
32
    errors,
31
 
    remote,
32
33
    trace,
33
34
    transport,
34
35
    urlutils,
35
36
    )
36
 
from bzrlib.smart import client, medium
37
 
from bzrlib.symbol_versioning import (deprecated_method, one_four)
 
37
from bzrlib.smart import client, medium, protocol
38
38
 
39
39
 
40
40
class _SmartStat(object):
61
61
    RemoteTCPTransport, etc.
62
62
    """
63
63
 
64
 
    # When making a readv request, cap it at requesting 5MB of data
65
 
    _max_readv_bytes = 5*1024*1024
66
 
 
67
64
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
68
65
    # responsibilities: Put those on SmartClient or similar. This is vital for
69
66
    # the ability to support multiple versions of the smart protocol over time:
81
78
            one is being cloned from.  Attributes such as the medium will
82
79
            be reused.
83
80
 
84
 
        :param medium: The medium to use for this RemoteTransport.  If None,
85
 
            the medium from the _from_transport is shared.  If both this
86
 
            and _from_transport are None, a new medium will be built.
87
 
            _from_transport and medium cannot both be specified.
 
81
        :param medium: The medium to use for this RemoteTransport. This must be
 
82
            supplied if _from_transport is None.
88
83
 
89
84
        :param _client: Override the _SmartClient used by this transport.  This
90
85
            should only be used for testing purposes; normally this is
109
104
            self._shared_connection = transport._SharedConnection(medium,
110
105
                                                                  credentials,
111
106
                                                                  self.base)
112
 
        elif medium is None:
113
 
            # No medium was specified, so share the medium from the
114
 
            # _from_transport.
115
 
            medium = self._shared_connection.connection
116
 
        else:
117
 
            raise AssertionError(
118
 
                "Both _from_transport (%r) and medium (%r) passed to "
119
 
                "RemoteTransport.__init__, but these parameters are mutally "
120
 
                "exclusive." % (_from_transport, medium))
121
107
 
122
108
        if _client is None:
123
 
            self._client = client._SmartClient(medium)
 
109
            self._client = client._SmartClient(self.get_shared_medium())
124
110
        else:
125
111
            self._client = _client
126
112
 
135
121
 
136
122
    def is_readonly(self):
137
123
        """Smart server transport can do read/write file operations."""
138
 
        try:
139
 
            resp = self._call2('Transport.is_readonly')
140
 
        except errors.UnknownSmartMethod:
 
124
        resp = self._call2('Transport.is_readonly')
 
125
        if resp == ('yes', ):
 
126
            return True
 
127
        elif resp == ('no', ):
 
128
            return False
 
129
        elif (resp == ('error', "Generic bzr smart protocol error: "
 
130
                                "bad request 'Transport.is_readonly'") or
 
131
              resp == ('error', "Generic bzr smart protocol error: "
 
132
                                "bad request u'Transport.is_readonly'")):
141
133
            # XXX: nasty hack: servers before 0.16 don't have a
142
134
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
143
135
            # did: assume False.
144
136
            return False
145
 
        if resp == ('yes', ):
146
 
            return True
147
 
        elif resp == ('no', ):
148
 
            return False
149
137
        else:
150
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
138
            self._translate_error(resp)
 
139
        raise errors.UnexpectedSmartServerResponse(resp)
151
140
 
152
141
    def get_smart_client(self):
153
142
        return self._get_connection()
155
144
    def get_smart_medium(self):
156
145
        return self._get_connection()
157
146
 
158
 
    @deprecated_method(one_four)
159
147
    def get_shared_medium(self):
160
148
        return self._get_shared_connection()
161
149
 
165
153
 
166
154
    def _call(self, method, *args):
167
155
        resp = self._call2(method, *args)
168
 
        self._ensure_ok(resp)
 
156
        self._translate_error(resp)
169
157
 
170
158
    def _call2(self, method, *args):
171
159
        """Call a method on the remote server."""
172
 
        try:
173
 
            return self._client.call(method, *args)
174
 
        except errors.ErrorFromSmartServer, err:
175
 
            # The first argument, if present, is always a path.
176
 
            if args:
177
 
                context = {'relpath': args[0]}
178
 
            else:
179
 
                context = {}
180
 
            self._translate_error(err, **context)
 
160
        return self._client.call(method, *args)
181
161
 
182
162
    def _call_with_body_bytes(self, method, args, body):
183
163
        """Call a method on the remote server with body bytes."""
184
 
        try:
185
 
            return self._client.call_with_body_bytes(method, args, body)
186
 
        except errors.ErrorFromSmartServer, err:
187
 
            # The first argument, if present, is always a path.
188
 
            if args:
189
 
                context = {'relpath': args[0]}
190
 
            else:
191
 
                context = {}
192
 
            self._translate_error(err, **context)
 
164
        return self._client.call_with_body_bytes(method, args, body)
193
165
 
194
166
    def has(self, relpath):
195
167
        """Indicate whether a remote file of the given name exists or not.
202
174
        elif resp == ('no', ):
203
175
            return False
204
176
        else:
205
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
177
            self._translate_error(resp)
206
178
 
207
179
    def get(self, relpath):
208
180
        """Return file-like object reading the contents of a remote file.
213
185
 
214
186
    def get_bytes(self, relpath):
215
187
        remote = self._remote_path(relpath)
216
 
        try:
217
 
            resp, response_handler = self._client.call_expecting_body('get', remote)
218
 
        except errors.ErrorFromSmartServer, err:
219
 
            self._translate_error(err, relpath)
 
188
        request = self.get_smart_medium().get_request()
 
189
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
190
        smart_protocol.call('get', remote)
 
191
        resp = smart_protocol.read_response_tuple(True)
220
192
        if resp != ('ok', ):
221
 
            response_handler.cancel_read_body()
222
 
            raise errors.UnexpectedSmartServerResponse(resp)
223
 
        return response_handler.read_body_bytes()
 
193
            smart_protocol.cancel_read_body()
 
194
            self._translate_error(resp, relpath)
 
195
        return smart_protocol.read_body_bytes()
224
196
 
225
197
    def _serialise_optional_mode(self, mode):
226
198
        if mode is None:
231
203
    def mkdir(self, relpath, mode=None):
232
204
        resp = self._call2('mkdir', self._remote_path(relpath),
233
205
            self._serialise_optional_mode(mode))
 
206
        self._translate_error(resp)
234
207
 
235
208
    def open_write_stream(self, relpath, mode=None):
236
209
        """See Transport.open_write_stream."""
252
225
        resp = self._call_with_body_bytes('put',
253
226
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
254
227
            upload_contents)
255
 
        self._ensure_ok(resp)
 
228
        self._translate_error(resp)
256
229
        return len(upload_contents)
257
230
 
258
231
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
269
242
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
270
243
             create_parent_str, self._serialise_optional_mode(dir_mode)),
271
244
            bytes)
272
 
        self._ensure_ok(resp)
 
245
        self._translate_error(resp)
273
246
 
274
247
    def put_file(self, relpath, upload_file, mode=None):
275
248
        # its not ideal to seek back, but currently put_non_atomic_file depends
299
272
            bytes)
300
273
        if resp[0] == 'appended':
301
274
            return int(resp[1])
302
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
275
        self._translate_error(resp)
303
276
 
304
277
    def delete(self, relpath):
305
278
        resp = self._call2('delete', self._remote_path(relpath))
306
 
        self._ensure_ok(resp)
 
279
        self._translate_error(resp)
307
280
 
308
281
    def external_url(self):
309
282
        """See bzrlib.transport.Transport.external_url."""
321
294
        offsets = list(offsets)
322
295
 
323
296
        sorted_offsets = sorted(offsets)
 
297
        # turn the list of offsets into a stack
 
298
        offset_stack = iter(offsets)
 
299
        cur_offset_and_size = offset_stack.next()
324
300
        coalesced = list(self._coalesce_offsets(sorted_offsets,
325
301
                               limit=self._max_readv_combine,
326
 
                               fudge_factor=self._bytes_to_read_before_seek,
327
 
                               max_size=self._max_readv_bytes))
328
 
 
329
 
        # now that we've coallesced things, avoid making enormous requests
330
 
        requests = []
331
 
        cur_request = []
332
 
        cur_len = 0
333
 
        for c in coalesced:
334
 
            if c.length + cur_len > self._max_readv_bytes:
335
 
                requests.append(cur_request)
336
 
                cur_request = [c]
337
 
                cur_len = c.length
338
 
                continue
339
 
            cur_request.append(c)
340
 
            cur_len += c.length
341
 
        if cur_request:
342
 
            requests.append(cur_request)
343
 
        if 'hpss' in debug.debug_flags:
344
 
            trace.mutter('%s.readv %s offsets => %s coalesced'
345
 
                         ' => %s requests (%s)',
346
 
                         self.__class__.__name__, len(offsets), len(coalesced),
347
 
                         len(requests), sum(map(len, requests)))
 
302
                               fudge_factor=self._bytes_to_read_before_seek))
 
303
 
 
304
        request = self.get_smart_medium().get_request()
 
305
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
306
        smart_protocol.call_with_body_readv_array(
 
307
            ('readv', self._remote_path(relpath)),
 
308
            [(c.start, c.length) for c in coalesced])
 
309
        resp = smart_protocol.read_response_tuple(True)
 
310
 
 
311
        if resp[0] != 'readv':
 
312
            # This should raise an exception
 
313
            smart_protocol.cancel_read_body()
 
314
            self._translate_error(resp)
 
315
            return
 
316
 
 
317
        # FIXME: this should know how many bytes are needed, for clarity.
 
318
        data = smart_protocol.read_body_bytes()
348
319
        # Cache the results, but only until they have been fulfilled
349
320
        data_map = {}
350
 
        # turn the list of offsets into a single stack to iterate
351
 
        offset_stack = iter(offsets)
352
 
        # using a list so it can be modified when passing down and coming back
353
 
        next_offset = [offset_stack.next()]
354
 
        for cur_request in requests:
355
 
            try:
356
 
                result = self._client.call_with_body_readv_array(
357
 
                    ('readv', self._remote_path(relpath),),
358
 
                    [(c.start, c.length) for c in cur_request])
359
 
                resp, response_handler = result
360
 
            except errors.ErrorFromSmartServer, err:
361
 
                self._translate_error(err, relpath)
362
 
 
363
 
            if resp[0] != 'readv':
364
 
                # This should raise an exception
365
 
                response_handler.cancel_read_body()
366
 
                raise errors.UnexpectedSmartServerResponse(resp)
367
 
 
368
 
            for res in self._handle_response(offset_stack, cur_request,
369
 
                                             response_handler,
370
 
                                             data_map,
371
 
                                             next_offset):
372
 
                yield res
373
 
 
374
 
    def _handle_response(self, offset_stack, coalesced, response_handler,
375
 
                         data_map, next_offset):
376
 
        cur_offset_and_size = next_offset[0]
377
 
        # FIXME: this should know how many bytes are needed, for clarity.
378
 
        data = response_handler.read_body_bytes()
379
 
        data_offset = 0
380
321
        for c_offset in coalesced:
381
322
            if len(data) < c_offset.length:
382
323
                raise errors.ShortReadvError(relpath, c_offset.start,
383
324
                            c_offset.length, actual=len(data))
384
325
            for suboffset, subsize in c_offset.ranges:
385
326
                key = (c_offset.start+suboffset, subsize)
386
 
                this_data = data[data_offset+suboffset:
387
 
                                 data_offset+suboffset+subsize]
388
 
                # Special case when the data is in-order, rather than packing
389
 
                # into a map and then back out again. Benchmarking shows that
390
 
                # this has 100% hit rate, but leave in the data_map work just
391
 
                # in case.
392
 
                # TODO: Could we get away with using buffer() to avoid the
393
 
                #       memory copy?  Callers would need to realize they may
394
 
                #       not have a real string.
395
 
                if key == cur_offset_and_size:
396
 
                    yield cur_offset_and_size[0], this_data
397
 
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
398
 
                else:
399
 
                    data_map[key] = this_data
400
 
            data_offset += c_offset.length
 
327
                data_map[key] = data[suboffset:suboffset+subsize]
 
328
            data = data[c_offset.length:]
401
329
 
402
330
            # Now that we've read some data, see if we can yield anything back
403
331
            while cur_offset_and_size in data_map:
404
332
                this_data = data_map.pop(cur_offset_and_size)
405
333
                yield cur_offset_and_size[0], this_data
406
 
                cur_offset_and_size = next_offset[0] = offset_stack.next()
 
334
                cur_offset_and_size = offset_stack.next()
407
335
 
408
336
    def rename(self, rel_from, rel_to):
409
337
        self._call('rename',
418
346
    def rmdir(self, relpath):
419
347
        resp = self._call('rmdir', self._remote_path(relpath))
420
348
 
421
 
    def _ensure_ok(self, resp):
422
 
        if resp[0] != 'ok':
423
 
            raise errors.UnexpectedSmartServerResponse(resp)
424
 
        
425
 
    def _translate_error(self, err, relpath=None):
426
 
        remote._translate_error(err, path=relpath)
 
349
    def _translate_error(self, resp, orig_path=None):
 
350
        """Raise an exception from a response"""
 
351
        if resp is None:
 
352
            what = None
 
353
        else:
 
354
            what = resp[0]
 
355
        if what == 'ok':
 
356
            return
 
357
        elif what == 'NoSuchFile':
 
358
            if orig_path is not None:
 
359
                error_path = orig_path
 
360
            else:
 
361
                error_path = resp[1]
 
362
            raise errors.NoSuchFile(error_path)
 
363
        elif what == 'error':
 
364
            raise errors.SmartProtocolError(unicode(resp[1]))
 
365
        elif what == 'FileExists':
 
366
            raise errors.FileExists(resp[1])
 
367
        elif what == 'DirectoryNotEmpty':
 
368
            raise errors.DirectoryNotEmpty(resp[1])
 
369
        elif what == 'ShortReadvError':
 
370
            raise errors.ShortReadvError(resp[1], int(resp[2]),
 
371
                                         int(resp[3]), int(resp[4]))
 
372
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
 
373
            encoding = str(resp[1]) # encoding must always be a string
 
374
            val = resp[2]
 
375
            start = int(resp[3])
 
376
            end = int(resp[4])
 
377
            reason = str(resp[5]) # reason must always be a string
 
378
            if val.startswith('u:'):
 
379
                val = val[2:].decode('utf-8')
 
380
            elif val.startswith('s:'):
 
381
                val = val[2:].decode('base64')
 
382
            if what == 'UnicodeDecodeError':
 
383
                raise UnicodeDecodeError(encoding, val, start, end, reason)
 
384
            elif what == 'UnicodeEncodeError':
 
385
                raise UnicodeEncodeError(encoding, val, start, end, reason)
 
386
        elif what == "ReadOnlyError":
 
387
            raise errors.TransportNotPossible('readonly transport')
 
388
        elif what == "ReadError":
 
389
            if orig_path is not None:
 
390
                error_path = orig_path
 
391
            else:
 
392
                error_path = resp[1]
 
393
            raise errors.ReadError(error_path)
 
394
        elif what == "PermissionDenied":
 
395
            if orig_path is not None:
 
396
                error_path = orig_path
 
397
            else:
 
398
                error_path = resp[1]
 
399
            raise errors.PermissionDenied(error_path)
 
400
        else:
 
401
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
427
402
 
428
403
    def disconnect(self):
429
404
        self.get_smart_medium().disconnect()
430
405
 
 
406
    def delete_tree(self, relpath):
 
407
        raise errors.TransportNotPossible('readonly transport')
 
408
 
431
409
    def stat(self, relpath):
432
410
        resp = self._call2('stat', self._remote_path(relpath))
433
411
        if resp[0] == 'stat':
434
412
            return _SmartStat(int(resp[1]), int(resp[2], 8))
435
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
413
        else:
 
414
            self._translate_error(resp)
436
415
 
437
416
    ## def lock_read(self, relpath):
438
417
    ##     """Lock the given file for shared (read) access.
454
433
        resp = self._call2('list_dir', self._remote_path(relpath))
455
434
        if resp[0] == 'names':
456
435
            return [name.encode('ascii') for name in resp[1:]]
457
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
436
        else:
 
437
            self._translate_error(resp)
458
438
 
459
439
    def iter_files_recursive(self):
460
440
        resp = self._call2('iter_files_recursive', self._remote_path(''))
461
441
        if resp[0] == 'names':
462
442
            return resp[1:]
463
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
443
        else:
 
444
            self._translate_error(resp)
464
445
 
465
446
 
466
447
class RemoteTCPTransport(RemoteTransport):
471
452
    """
472
453
 
473
454
    def _build_medium(self):
474
 
        client_medium = medium.SmartTCPClientMedium(
475
 
            self._host, self._port, self.base)
476
 
        return client_medium, None
477
 
 
478
 
 
479
 
class RemoteTCPTransportV2Only(RemoteTransport):
480
 
    """Connection to smart server over plain tcp with the client hard-coded to
481
 
    assume protocol v2 and remote server version <= 1.6.
482
 
 
483
 
    This should only be used for testing.
484
 
    """
485
 
 
486
 
    def _build_medium(self):
487
 
        client_medium = medium.SmartTCPClientMedium(
488
 
            self._host, self._port, self.base)
489
 
        client_medium._protocol_version = 2
490
 
        client_medium._remember_remote_is_before((1, 6))
491
 
        return client_medium, None
 
455
        assert self.base.startswith('bzr://')
 
456
        return medium.SmartTCPClientMedium(self._host, self._port), None
492
457
 
493
458
 
494
459
class RemoteSSHTransport(RemoteTransport):
499
464
    """
500
465
 
501
466
    def _build_medium(self):
 
467
        assert self.base.startswith('bzr+ssh://')
 
468
        # ssh will prompt the user for a password if needed and if none is
 
469
        # provided but it will not give it back, so no credentials can be
 
470
        # stored.
502
471
        location_config = config.LocationConfig(self.base)
503
472
        bzr_remote_path = location_config.get_bzr_remote_path()
504
 
        user = self._user
505
 
        if user is None:
506
 
            auth = config.AuthenticationConfig()
507
 
            user = auth.get_user('ssh', self._host, self._port)
508
 
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
509
 
            user, self._password, self.base,
510
 
            bzr_remote_path=bzr_remote_path)
511
 
        return client_medium, (user, self._password)
 
473
        return medium.SmartSSHClientMedium(self._host, self._port,
 
474
            self._user, self._password, bzr_remote_path=bzr_remote_path), None
512
475
 
513
476
 
514
477
class RemoteHTTPTransport(RemoteTransport):
523
486
    """
524
487
 
525
488
    def __init__(self, base, _from_transport=None, http_transport=None):
 
489
        assert ( base.startswith('bzr+http://') or base.startswith('bzr+https://') )
 
490
 
526
491
        if http_transport is None:
527
492
            # FIXME: the password may be lost here because it appears in the
528
493
            # url only for an intial construction (when the url came from the