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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""RemoteTransport client for the smart-server.
63
60
RemoteTCPTransport, etc.
66
# When making a readv request, cap it at requesting 5MB of data
67
_max_readv_bytes = 5*1024*1024
69
63
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
70
64
# responsibilities: Put those on SmartClient or similar. This is vital for
71
65
# the ability to support multiple versions of the smart protocol over time:
72
# RemoteTransport is an adapter from the Transport object model to the
66
# RemoteTransport is an adapter from the Transport object model to the
73
67
# SmartClient model, not an encoder.
75
69
# FIXME: the medium parameter should be private, only the tests requires
92
86
should only be used for testing purposes; normally this is
93
87
determined from the medium.
95
super(RemoteTransport, self).__init__(
96
url, _from_transport=_from_transport)
89
super(RemoteTransport, self).__init__(url,
90
_from_transport=_from_transport)
98
92
# The medium is the connection, except when we need to share it with
99
93
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
94
# 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:
96
if _from_transport is None:
106
97
# If no _from_transport is specified, we need to intialize the
108
99
credentials = None
168
152
def get_smart_medium(self):
169
153
return self._get_connection()
155
@deprecated_method(one_four)
156
def get_shared_medium(self):
157
return self._get_shared_connection()
171
159
def _remote_path(self, relpath):
172
160
"""Returns the Unicode version of the absolute path for relpath."""
173
return urlutils.URL._combine_paths(self._parsed_url.path, relpath)
161
return self._combine_paths(self._path, relpath)
175
163
def _call(self, method, *args):
176
resp = self._call2(method, *args)
177
self._ensure_ok(resp)
165
resp = self._call2(method, *args)
166
except errors.ErrorFromSmartServer, err:
167
self._translate_error(err.error_tuple)
168
self._translate_error(resp)
179
170
def _call2(self, method, *args):
180
171
"""Call a method on the remote server."""
182
173
return self._client.call(method, *args)
183
174
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)
175
self._translate_error(err.error_tuple)
191
177
def _call_with_body_bytes(self, method, args, body):
192
178
"""Call a method on the remote server with body bytes."""
194
180
return self._client.call_with_body_bytes(method, args, body)
195
181
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)
182
self._translate_error(err.error_tuple)
203
184
def has(self, relpath):
204
185
"""Indicate whether a remote file of the given name exists or not.
332
314
sorted_offsets = sorted(offsets)
333
315
coalesced = list(self._coalesce_offsets(sorted_offsets,
334
316
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)))
357
# Cache the results, but only until they have been fulfilled
359
# turn the list of offsets into a single stack to iterate
317
fudge_factor=self._bytes_to_read_before_seek))
320
result = self._client.call_with_body_readv_array(
321
('readv', self._remote_path(relpath),),
322
[(c.start, c.length) for c in coalesced])
323
resp, response_handler = result
324
except errors.ErrorFromSmartServer, err:
325
self._translate_error(err.error_tuple)
327
if resp[0] != 'readv':
328
# This should raise an exception
329
response_handler.cancel_read_body()
330
raise errors.UnexpectedSmartServerResponse(resp)
332
return self._handle_response(offsets, coalesced, response_handler)
334
def _handle_response(self, offsets, coalesced, response_handler):
335
# turn the list of offsets into a stack
360
336
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]
337
cur_offset_and_size = offset_stack.next()
386
338
# FIXME: this should know how many bytes are needed, for clarity.
387
339
data = response_handler.read_body_bytes()
340
# Cache the results, but only until they have been fulfilled
389
343
for c_offset in coalesced:
390
344
if len(data) < c_offset.length:
427
381
def rmdir(self, relpath):
428
382
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)
384
def _translate_error(self, resp, orig_path=None):
385
"""Raise an exception from a response"""
392
elif what == 'NoSuchFile':
393
if orig_path is not None:
394
error_path = orig_path
397
raise errors.NoSuchFile(error_path)
398
elif what == 'error':
399
raise errors.SmartProtocolError(unicode(resp[1]))
400
elif what == 'FileExists':
401
raise errors.FileExists(resp[1])
402
elif what == 'DirectoryNotEmpty':
403
raise errors.DirectoryNotEmpty(resp[1])
404
elif what == 'ShortReadvError':
405
raise errors.ShortReadvError(resp[1], int(resp[2]),
406
int(resp[3]), int(resp[4]))
407
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
408
encoding = str(resp[1]) # encoding must always be a string
412
reason = str(resp[5]) # reason must always be a string
413
if val.startswith('u:'):
414
val = val[2:].decode('utf-8')
415
elif val.startswith('s:'):
416
val = val[2:].decode('base64')
417
if what == 'UnicodeDecodeError':
418
raise UnicodeDecodeError(encoding, val, start, end, reason)
419
elif what == 'UnicodeEncodeError':
420
raise UnicodeEncodeError(encoding, val, start, end, reason)
421
elif what == "ReadOnlyError":
422
raise errors.TransportNotPossible('readonly transport')
423
elif what == "ReadError":
424
if orig_path is not None:
425
error_path = orig_path
428
raise errors.ReadError(error_path)
429
elif what == "PermissionDenied":
430
if orig_path is not None:
431
error_path = orig_path
434
raise errors.PermissionDenied(error_path)
436
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
437
438
def disconnect(self):
438
m = self.get_smart_medium()
439
self.get_smart_medium().disconnect()
442
441
def stat(self, relpath):
443
442
resp = self._call2('stat', self._remote_path(relpath))
444
443
if resp[0] == 'stat':
445
444
return _SmartStat(int(resp[1]), int(resp[2], 8))
446
raise errors.UnexpectedSmartServerResponse(resp)
446
self._translate_error(resp)
448
448
## def lock_read(self, relpath):
449
449
## """Lock the given file for shared (read) access.
465
465
resp = self._call2('list_dir', self._remote_path(relpath))
466
466
if resp[0] == 'names':
467
467
return [name.encode('ascii') for name in resp[1:]]
468
raise errors.UnexpectedSmartServerResponse(resp)
469
self._translate_error(resp)
470
471
def iter_files_recursive(self):
471
472
resp = self._call2('iter_files_recursive', self._remote_path(''))
472
473
if resp[0] == 'names':
474
raise errors.UnexpectedSmartServerResponse(resp)
476
self._translate_error(resp)
477
479
class RemoteTCPTransport(RemoteTransport):
478
480
"""Connection to smart server over plain tcp.
480
482
This is essentially just a factory to get 'RemoteTransport(url,
481
483
SmartTCPClientMedium).
484
486
def _build_medium(self):
485
487
client_medium = medium.SmartTCPClientMedium(
486
self._parsed_url.host, self._parsed_url.port, self.base)
488
self._host, self._port, self.base)
487
489
return client_medium, None
512
514
def _build_medium(self):
515
# ssh will prompt the user for a password if needed and if none is
516
# provided but it will not give it back, so no credentials can be
513
518
location_config = config.LocationConfig(self.base)
514
519
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)
520
client_medium = medium.SmartSSHClientMedium(self._host, self._port,
521
self._user, self._password, self.base,
522
bzr_remote_path=bzr_remote_path)
523
return client_medium, None
527
526
class RemoteHTTPTransport(RemoteTransport):
528
527
"""Just a way to connect between a bzr+http:// url and http://.
530
529
This connection operates slightly differently than the RemoteSSHTransport.
531
530
It uses a plain http:// transport underneath, which defines what remote
532
531
.bzr/smart URL we are connected to. From there, all paths that are sent are
581
580
_from_transport=self,
582
581
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
584
def get_test_permutations():
605
585
"""Return (transport, server) permutations for testing."""
606
586
### We may need a little more test framework support to construct an
607
587
### appropriate RemoteTransport in the future.
608
from bzrlib.tests import test_server
609
return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]
588
from bzrlib.smart import server
589
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]