83
78
one is being cloned from. Attributes such as the medium will
86
:param medium: The medium to use for this RemoteTransport. If None,
87
the medium from the _from_transport is shared. If both this
88
and _from_transport are None, a new medium will be built.
89
_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.
91
84
:param _client: Override the _SmartClient used by this transport. This
92
85
should only be used for testing purposes; normally this is
93
86
determined from the medium.
95
super(RemoteTransport, self).__init__(
96
url, _from_transport=_from_transport)
88
super(RemoteTransport, self).__init__(url,
89
_from_transport=_from_transport)
98
91
# The medium is the connection, except when we need to share it with
99
92
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
93
# what we want to share is really the shared connection.
102
if (_from_transport is not None
103
and isinstance(_from_transport, RemoteTransport)):
104
_client = _from_transport._client
105
elif _from_transport is None:
95
if _from_transport is None:
106
96
# If no _from_transport is specified, we need to intialize the
108
98
credentials = None
139
120
return None, None
141
def _report_activity(self, bytes, direction):
142
"""See Transport._report_activity.
144
Does nothing; the smart medium will report activity triggered by a
149
122
def is_readonly(self):
150
123
"""Smart server transport can do read/write file operations."""
152
resp = self._call2('Transport.is_readonly')
153
except errors.UnknownSmartMethod:
124
resp = self._call2('Transport.is_readonly')
125
if resp == ('yes', ):
127
elif resp == ('no', ):
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'")):
154
133
# XXX: nasty hack: servers before 0.16 don't have a
155
134
# 'Transport.is_readonly' verb, so we do what clients before 0.16
156
135
# did: assume False.
158
if resp == ('yes', ):
160
elif resp == ('no', ):
163
raise errors.UnexpectedSmartServerResponse(resp)
138
self._translate_error(resp)
139
raise errors.UnexpectedSmartServerResponse(resp)
165
141
def get_smart_client(self):
166
142
return self._get_connection()
168
144
def get_smart_medium(self):
169
145
return self._get_connection()
147
def get_shared_medium(self):
148
return self._get_shared_connection()
171
150
def _remote_path(self, relpath):
172
151
"""Returns the Unicode version of the absolute path for relpath."""
173
return urlutils.URL._combine_paths(self._parsed_url.path, relpath)
152
return self._combine_paths(self._path, relpath)
175
154
def _call(self, method, *args):
176
155
resp = self._call2(method, *args)
177
self._ensure_ok(resp)
156
self._translate_error(resp)
179
158
def _call2(self, method, *args):
180
159
"""Call a method on the remote server."""
182
return self._client.call(method, *args)
183
except errors.ErrorFromSmartServer, err:
184
# The first argument, if present, is always a path.
186
context = {'relpath': args[0]}
189
self._translate_error(err, **context)
160
return self._client.call(method, *args)
191
162
def _call_with_body_bytes(self, method, args, body):
192
163
"""Call a method on the remote server with body bytes."""
194
return self._client.call_with_body_bytes(method, args, body)
195
except errors.ErrorFromSmartServer, err:
196
# The first argument, if present, is always a path.
198
context = {'relpath': args[0]}
201
self._translate_error(err, **context)
164
return self._client.call_with_body_bytes(method, args, body)
203
166
def has(self, relpath):
204
167
"""Indicate whether a remote file of the given name exists or not.
211
174
elif resp == ('no', ):
214
raise errors.UnexpectedSmartServerResponse(resp)
177
self._translate_error(resp)
216
179
def get(self, relpath):
217
180
"""Return file-like object reading the contents of a remote file.
219
182
:see: Transport.get_bytes()/get_file()
221
184
return StringIO(self.get_bytes(relpath))
223
186
def get_bytes(self, relpath):
224
187
remote = self._remote_path(relpath)
226
resp, response_handler = self._client.call_expecting_body('get', remote)
227
except errors.ErrorFromSmartServer, err:
228
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)
229
192
if resp != ('ok', ):
230
response_handler.cancel_read_body()
231
raise errors.UnexpectedSmartServerResponse(resp)
232
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()
234
197
def _serialise_optional_mode(self, mode):
330
294
offsets = list(offsets)
332
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()
333
300
coalesced = list(self._coalesce_offsets(sorted_offsets,
334
301
limit=self._max_readv_combine,
335
fudge_factor=self._bytes_to_read_before_seek,
336
max_size=self._max_readv_bytes))
338
# now that we've coallesced things, avoid making enormous requests
343
if c.length + cur_len > self._max_readv_bytes:
344
requests.append(cur_request)
348
cur_request.append(c)
351
requests.append(cur_request)
352
if 'hpss' in debug.debug_flags:
353
trace.mutter('%s.readv %s offsets => %s coalesced'
354
' => %s requests (%s)',
355
self.__class__.__name__, len(offsets), len(coalesced),
356
len(requests), sum(map(len, requests)))
302
fudge_factor=self._bytes_to_read_before_seek))
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)
311
if resp[0] != 'readv':
312
# This should raise an exception
313
smart_protocol.cancel_read_body()
314
self._translate_error(resp)
317
# FIXME: this should know how many bytes are needed, for clarity.
318
data = smart_protocol.read_body_bytes()
357
319
# Cache the results, but only until they have been fulfilled
359
# turn the list of offsets into a single stack to iterate
360
offset_stack = iter(offsets)
361
# using a list so it can be modified when passing down and coming back
362
next_offset = [offset_stack.next()]
363
for cur_request in requests:
365
result = self._client.call_with_body_readv_array(
366
('readv', self._remote_path(relpath),),
367
[(c.start, c.length) for c in cur_request])
368
resp, response_handler = result
369
except errors.ErrorFromSmartServer, err:
370
self._translate_error(err, relpath)
372
if resp[0] != 'readv':
373
# This should raise an exception
374
response_handler.cancel_read_body()
375
raise errors.UnexpectedSmartServerResponse(resp)
377
for res in self._handle_response(offset_stack, cur_request,
383
def _handle_response(self, offset_stack, coalesced, response_handler,
384
data_map, next_offset):
385
cur_offset_and_size = next_offset[0]
386
# FIXME: this should know how many bytes are needed, for clarity.
387
data = response_handler.read_body_bytes()
389
321
for c_offset in coalesced:
390
322
if len(data) < c_offset.length:
391
323
raise errors.ShortReadvError(relpath, c_offset.start,
392
324
c_offset.length, actual=len(data))
393
325
for suboffset, subsize in c_offset.ranges:
394
326
key = (c_offset.start+suboffset, subsize)
395
this_data = data[data_offset+suboffset:
396
data_offset+suboffset+subsize]
397
# Special case when the data is in-order, rather than packing
398
# into a map and then back out again. Benchmarking shows that
399
# this has 100% hit rate, but leave in the data_map work just
401
# TODO: Could we get away with using buffer() to avoid the
402
# memory copy? Callers would need to realize they may
403
# not have a real string.
404
if key == cur_offset_and_size:
405
yield cur_offset_and_size[0], this_data
406
cur_offset_and_size = next_offset[0] = offset_stack.next()
408
data_map[key] = this_data
409
data_offset += c_offset.length
327
data_map[key] = data[suboffset:suboffset+subsize]
328
data = data[c_offset.length:]
411
330
# Now that we've read some data, see if we can yield anything back
412
331
while cur_offset_and_size in data_map:
413
332
this_data = data_map.pop(cur_offset_and_size)
414
333
yield cur_offset_and_size[0], this_data
415
cur_offset_and_size = next_offset[0] = offset_stack.next()
334
cur_offset_and_size = offset_stack.next()
417
336
def rename(self, rel_from, rel_to):
418
337
self._call('rename',
427
346
def rmdir(self, relpath):
428
347
resp = self._call('rmdir', self._remote_path(relpath))
430
def _ensure_ok(self, resp):
432
raise errors.UnexpectedSmartServerResponse(resp)
434
def _translate_error(self, err, relpath=None):
435
remote._translate_error(err, path=relpath)
349
def _translate_error(self, resp, orig_path=None):
350
"""Raise an exception from a response"""
357
elif what == 'NoSuchFile':
358
if orig_path is not None:
359
error_path = orig_path
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
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
393
raise errors.ReadError(error_path)
394
elif what == "PermissionDenied":
395
if orig_path is not None:
396
error_path = orig_path
399
raise errors.PermissionDenied(error_path)
401
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
437
403
def disconnect(self):
438
m = self.get_smart_medium()
404
self.get_smart_medium().disconnect()
406
def delete_tree(self, relpath):
407
raise errors.TransportNotPossible('readonly transport')
442
409
def stat(self, relpath):
443
410
resp = self._call2('stat', self._remote_path(relpath))
444
411
if resp[0] == 'stat':
445
412
return _SmartStat(int(resp[1]), int(resp[2], 8))
446
raise errors.UnexpectedSmartServerResponse(resp)
414
self._translate_error(resp)
448
416
## def lock_read(self, relpath):
449
417
## """Lock the given file for shared (read) access.
465
433
resp = self._call2('list_dir', self._remote_path(relpath))
466
434
if resp[0] == 'names':
467
435
return [name.encode('ascii') for name in resp[1:]]
468
raise errors.UnexpectedSmartServerResponse(resp)
437
self._translate_error(resp)
470
439
def iter_files_recursive(self):
471
440
resp = self._call2('iter_files_recursive', self._remote_path(''))
472
441
if resp[0] == 'names':
474
raise errors.UnexpectedSmartServerResponse(resp)
444
self._translate_error(resp)
477
447
class RemoteTCPTransport(RemoteTransport):
478
448
"""Connection to smart server over plain tcp.
480
450
This is essentially just a factory to get 'RemoteTransport(url,
481
451
SmartTCPClientMedium).
484
454
def _build_medium(self):
485
client_medium = medium.SmartTCPClientMedium(
486
self._parsed_url.host, self._parsed_url.port, self.base)
487
return client_medium, None
490
class RemoteTCPTransportV2Only(RemoteTransport):
491
"""Connection to smart server over plain tcp with the client hard-coded to
492
assume protocol v2 and remote server version <= 1.6.
494
This should only be used for testing.
497
def _build_medium(self):
498
client_medium = medium.SmartTCPClientMedium(
499
self._parsed_url.host, self._parsed_url.port, self.base)
500
client_medium._protocol_version = 2
501
client_medium._remember_remote_is_before((1, 6))
502
return client_medium, None
455
assert self.base.startswith('bzr://')
456
return medium.SmartTCPClientMedium(self._host, self._port), None
505
459
class RemoteSSHTransport(RemoteTransport):
512
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
513
471
location_config = config.LocationConfig(self.base)
514
472
bzr_remote_path = location_config.get_bzr_remote_path()
515
user = self._parsed_url.user
517
auth = config.AuthenticationConfig()
518
user = auth.get_user('ssh', self._parsed_url.host,
519
self._parsed_url.port)
520
ssh_params = medium.SSHParams(self._parsed_url.host,
521
self._parsed_url.port, user, self._parsed_url.password,
523
client_medium = medium.SmartSSHClientMedium(self.base, ssh_params)
524
return client_medium, (user, self._parsed_url.password)
473
return medium.SmartSSHClientMedium(self._host, self._port,
474
self._user, self._password, bzr_remote_path=bzr_remote_path), None
527
477
class RemoteHTTPTransport(RemoteTransport):
528
478
"""Just a way to connect between a bzr+http:// url and http://.
530
480
This connection operates slightly differently than the RemoteSSHTransport.
531
481
It uses a plain http:// transport underneath, which defines what remote
532
482
.bzr/smart URL we are connected to. From there, all paths that are sent are
581
533
_from_transport=self,
582
534
http_transport=self._http_transport)
584
def _redirected_to(self, source, target):
585
"""See transport._redirected_to"""
586
redirected = self._http_transport._redirected_to(source, target)
587
if (redirected is not None
588
and isinstance(redirected, type(self._http_transport))):
589
return RemoteHTTPTransport('bzr+' + redirected.external_url(),
590
http_transport=redirected)
592
# Either None or a transport for a different protocol
596
class HintingSSHTransport(transport.Transport):
597
"""Simple transport that handles ssh:// and points out bzr+ssh://."""
599
def __init__(self, url):
600
raise errors.UnsupportedProtocol(url,
601
'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
604
537
def get_test_permutations():
605
538
"""Return (transport, server) permutations for testing."""
606
539
### We may need a little more test framework support to construct an
607
540
### appropriate RemoteTransport in the future.
608
from bzrlib.tests import test_server
609
return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]
541
from bzrlib.smart import server
542
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]