1
# Copyright (C) 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""RemoteTransport client for the smart-server.
19
This module shouldn't be accessed directly. The classes defined here should be
20
imported from bzrlib.smart.
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
25
from cStringIO import StringIO
36
from bzrlib.smart import client, medium
37
from bzrlib.symbol_versioning import (deprecated_method, one_four)
40
class _SmartStat(object):
42
def __init__(self, size, mode):
47
class RemoteTransport(transport.ConnectedTransport):
48
"""Connection to a smart server.
50
The connection holds references to the medium that can be used to send
51
requests to the server.
53
The connection has a notion of the current directory to which it's
54
connected; this is incorporated in filenames passed to the server.
56
This supports some higher-level RPC operations and can also be treated
57
like a Transport to do file-like operations.
59
The connection can be made over a tcp socket, an ssh pipe or a series of
60
http requests. There are concrete subclasses for each type:
61
RemoteTCPTransport, etc.
64
# When making a readv request, cap it at requesting 5MB of data
65
_max_readv_bytes = 5*1024*1024
67
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
68
# responsibilities: Put those on SmartClient or similar. This is vital for
69
# the ability to support multiple versions of the smart protocol over time:
70
# RemoteTransport is an adapter from the Transport object model to the
71
# SmartClient model, not an encoder.
73
# FIXME: the medium parameter should be private, only the tests requires
74
# it. It may be even clearer to define a TestRemoteTransport that handles
75
# the specific cases of providing a _client and/or a _medium, and leave
76
# RemoteTransport as an abstract class.
77
def __init__(self, url, _from_transport=None, medium=None, _client=None):
80
:param _from_transport: Another RemoteTransport instance that this
81
one is being cloned from. Attributes such as the medium will
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.
89
:param _client: Override the _SmartClient used by this transport. This
90
should only be used for testing purposes; normally this is
91
determined from the medium.
93
super(RemoteTransport, self).__init__(url,
94
_from_transport=_from_transport)
96
# The medium is the connection, except when we need to share it with
97
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
98
# what we want to share is really the shared connection.
100
if _from_transport is None:
101
# If no _from_transport is specified, we need to intialize the
105
medium, credentials = self._build_medium()
106
if 'hpss' in debug.debug_flags:
107
trace.mutter('hpss: Built a new medium: %s',
108
medium.__class__.__name__)
109
self._shared_connection = transport._SharedConnection(medium,
113
# No medium was specified, so share the medium from the
115
medium = self._shared_connection.connection
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))
123
self._client = client._SmartClient(medium)
125
self._client = _client
127
def _build_medium(self):
128
"""Create the medium if _from_transport does not provide one.
130
The medium is analogous to the connection for ConnectedTransport: it
131
allows connection sharing.
136
def is_readonly(self):
137
"""Smart server transport can do read/write file operations."""
139
resp = self._call2('Transport.is_readonly')
140
except errors.UnknownSmartMethod:
141
# XXX: nasty hack: servers before 0.16 don't have a
142
# 'Transport.is_readonly' verb, so we do what clients before 0.16
145
if resp == ('yes', ):
147
elif resp == ('no', ):
150
raise errors.UnexpectedSmartServerResponse(resp)
152
def get_smart_client(self):
153
return self._get_connection()
155
def get_smart_medium(self):
156
return self._get_connection()
158
@deprecated_method(one_four)
159
def get_shared_medium(self):
160
return self._get_shared_connection()
162
def _remote_path(self, relpath):
163
"""Returns the Unicode version of the absolute path for relpath."""
164
return self._combine_paths(self._path, relpath)
166
def _call(self, method, *args):
167
resp = self._call2(method, *args)
168
self._ensure_ok(resp)
170
def _call2(self, method, *args):
171
"""Call a method on the remote server."""
173
return self._client.call(method, *args)
174
except errors.ErrorFromSmartServer, err:
175
self._translate_error(err)
177
def _call_with_body_bytes(self, method, args, body):
178
"""Call a method on the remote server with body bytes."""
180
return self._client.call_with_body_bytes(method, args, body)
181
except errors.ErrorFromSmartServer, err:
182
self._translate_error(err)
184
def has(self, relpath):
185
"""Indicate whether a remote file of the given name exists or not.
187
:see: Transport.has()
189
resp = self._call2('has', self._remote_path(relpath))
190
if resp == ('yes', ):
192
elif resp == ('no', ):
195
raise errors.UnexpectedSmartServerResponse(resp)
197
def get(self, relpath):
198
"""Return file-like object reading the contents of a remote file.
200
:see: Transport.get_bytes()/get_file()
202
return StringIO(self.get_bytes(relpath))
204
def get_bytes(self, relpath):
205
remote = self._remote_path(relpath)
207
resp, response_handler = self._client.call_expecting_body('get', remote)
208
except errors.ErrorFromSmartServer, err:
209
self._translate_error(err, relpath)
211
response_handler.cancel_read_body()
212
raise errors.UnexpectedSmartServerResponse(resp)
213
return response_handler.read_body_bytes()
215
def _serialise_optional_mode(self, mode):
221
def mkdir(self, relpath, mode=None):
222
resp = self._call2('mkdir', self._remote_path(relpath),
223
self._serialise_optional_mode(mode))
225
def open_write_stream(self, relpath, mode=None):
226
"""See Transport.open_write_stream."""
227
self.put_bytes(relpath, "", mode)
228
result = transport.AppendBasedFileStream(self, relpath)
229
transport._file_streams[self.abspath(relpath)] = result
232
def put_bytes(self, relpath, upload_contents, mode=None):
233
# FIXME: upload_file is probably not safe for non-ascii characters -
234
# should probably just pass all parameters as length-delimited
236
if type(upload_contents) is unicode:
237
# Although not strictly correct, we raise UnicodeEncodeError to be
238
# compatible with other transports.
239
raise UnicodeEncodeError(
240
'undefined', upload_contents, 0, 1,
241
'put_bytes must be given bytes, not unicode.')
242
resp = self._call_with_body_bytes('put',
243
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
245
self._ensure_ok(resp)
246
return len(upload_contents)
248
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
249
create_parent_dir=False,
251
"""See Transport.put_bytes_non_atomic."""
252
# FIXME: no encoding in the transport!
253
create_parent_str = 'F'
254
if create_parent_dir:
255
create_parent_str = 'T'
257
resp = self._call_with_body_bytes(
259
(self._remote_path(relpath), self._serialise_optional_mode(mode),
260
create_parent_str, self._serialise_optional_mode(dir_mode)),
262
self._ensure_ok(resp)
264
def put_file(self, relpath, upload_file, mode=None):
265
# its not ideal to seek back, but currently put_non_atomic_file depends
266
# on transports not reading before failing - which is a faulty
267
# assumption I think - RBC 20060915
268
pos = upload_file.tell()
270
return self.put_bytes(relpath, upload_file.read(), mode)
272
upload_file.seek(pos)
275
def put_file_non_atomic(self, relpath, f, mode=None,
276
create_parent_dir=False,
278
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
279
create_parent_dir=create_parent_dir,
282
def append_file(self, relpath, from_file, mode=None):
283
return self.append_bytes(relpath, from_file.read(), mode)
285
def append_bytes(self, relpath, bytes, mode=None):
286
resp = self._call_with_body_bytes(
288
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
290
if resp[0] == 'appended':
292
raise errors.UnexpectedSmartServerResponse(resp)
294
def delete(self, relpath):
295
resp = self._call2('delete', self._remote_path(relpath))
296
self._ensure_ok(resp)
298
def external_url(self):
299
"""See bzrlib.transport.Transport.external_url."""
300
# the external path for RemoteTransports is the base
303
def recommended_page_size(self):
304
"""Return the recommended page size for this transport."""
307
def _readv(self, relpath, offsets):
311
offsets = list(offsets)
313
sorted_offsets = sorted(offsets)
314
coalesced = list(self._coalesce_offsets(sorted_offsets,
315
limit=self._max_readv_combine,
316
fudge_factor=self._bytes_to_read_before_seek))
318
# now that we've coallesced things, avoid making enormous requests
323
if c.length + cur_len > self._max_readv_bytes:
324
requests.append(cur_request)
328
cur_request.append(c)
331
requests.append(cur_request)
332
if 'hpss' in debug.debug_flags:
333
trace.mutter('%s.readv %s offsets => %s coalesced'
334
' => %s requests (%s)',
335
self.__class__.__name__, len(offsets), len(coalesced),
336
len(requests), sum(map(len, requests)))
337
# Cache the results, but only until they have been fulfilled
339
# turn the list of offsets into a single stack to iterate
340
offset_stack = iter(offsets)
341
# using a list so it can be modified when passing down and coming back
342
next_offset = [offset_stack.next()]
343
for cur_request in requests:
345
result = self._client.call_with_body_readv_array(
346
('readv', self._remote_path(relpath),),
347
[(c.start, c.length) for c in cur_request])
348
resp, response_handler = result
349
except errors.ErrorFromSmartServer, err:
350
self._translate_error(err)
352
if resp[0] != 'readv':
353
# This should raise an exception
354
response_handler.cancel_read_body()
355
raise errors.UnexpectedSmartServerResponse(resp)
357
for res in self._handle_response(offset_stack, cur_request,
363
def _handle_response(self, offset_stack, coalesced, response_handler,
364
data_map, next_offset):
365
cur_offset_and_size = next_offset[0]
366
# FIXME: this should know how many bytes are needed, for clarity.
367
data = response_handler.read_body_bytes()
369
for c_offset in coalesced:
370
if len(data) < c_offset.length:
371
raise errors.ShortReadvError(relpath, c_offset.start,
372
c_offset.length, actual=len(data))
373
for suboffset, subsize in c_offset.ranges:
374
key = (c_offset.start+suboffset, subsize)
375
this_data = data[data_offset+suboffset:
376
data_offset+suboffset+subsize]
377
# Special case when the data is in-order, rather than packing
378
# into a map and then back out again. Benchmarking shows that
379
# this has 100% hit rate, but leave in the data_map work just
381
# TODO: Could we get away with using buffer() to avoid the
382
# memory copy? Callers would need to realize they may
383
# not have a real string.
384
if key == cur_offset_and_size:
385
yield cur_offset_and_size[0], this_data
386
cur_offset_and_size = next_offset[0] = offset_stack.next()
388
data_map[key] = this_data
389
data_offset += c_offset.length
391
# Now that we've read some data, see if we can yield anything back
392
while cur_offset_and_size in data_map:
393
this_data = data_map.pop(cur_offset_and_size)
394
yield cur_offset_and_size[0], this_data
395
cur_offset_and_size = next_offset[0] = offset_stack.next()
397
def rename(self, rel_from, rel_to):
399
self._remote_path(rel_from),
400
self._remote_path(rel_to))
402
def move(self, rel_from, rel_to):
404
self._remote_path(rel_from),
405
self._remote_path(rel_to))
407
def rmdir(self, relpath):
408
resp = self._call('rmdir', self._remote_path(relpath))
410
def _ensure_ok(self, resp):
412
raise errors.UnexpectedSmartServerResponse(resp)
414
def _translate_error(self, err, orig_path=None):
415
remote._translate_error(err, path=orig_path)
417
def disconnect(self):
418
self.get_smart_medium().disconnect()
420
def stat(self, relpath):
421
resp = self._call2('stat', self._remote_path(relpath))
422
if resp[0] == 'stat':
423
return _SmartStat(int(resp[1]), int(resp[2], 8))
424
raise errors.UnexpectedSmartServerResponse(resp)
426
## def lock_read(self, relpath):
427
## """Lock the given file for shared (read) access.
428
## :return: A lock object, which should be passed to Transport.unlock()
430
## # The old RemoteBranch ignore lock for reading, so we will
431
## # continue that tradition and return a bogus lock object.
432
## class BogusLock(object):
433
## def __init__(self, path):
437
## return BogusLock(relpath)
442
def list_dir(self, relpath):
443
resp = self._call2('list_dir', self._remote_path(relpath))
444
if resp[0] == 'names':
445
return [name.encode('ascii') for name in resp[1:]]
446
raise errors.UnexpectedSmartServerResponse(resp)
448
def iter_files_recursive(self):
449
resp = self._call2('iter_files_recursive', self._remote_path(''))
450
if resp[0] == 'names':
452
raise errors.UnexpectedSmartServerResponse(resp)
455
class RemoteTCPTransport(RemoteTransport):
456
"""Connection to smart server over plain tcp.
458
This is essentially just a factory to get 'RemoteTransport(url,
459
SmartTCPClientMedium).
462
def _build_medium(self):
463
client_medium = medium.SmartTCPClientMedium(
464
self._host, self._port, self.base)
465
return client_medium, None
468
class RemoteTCPTransportV2Only(RemoteTransport):
469
"""Connection to smart server over plain tcp with the client hard-coded to
470
assume protocol v2 and remote server version <= 1.6.
472
This should only be used for testing.
475
def _build_medium(self):
476
client_medium = medium.SmartTCPClientMedium(
477
self._host, self._port, self.base)
478
client_medium._protocol_version = 2
479
client_medium._remember_remote_is_before((1, 6))
480
return client_medium, None
483
class RemoteSSHTransport(RemoteTransport):
484
"""Connection to smart server over SSH.
486
This is essentially just a factory to get 'RemoteTransport(url,
487
SmartSSHClientMedium).
490
def _build_medium(self):
491
location_config = config.LocationConfig(self.base)
492
bzr_remote_path = location_config.get_bzr_remote_path()
495
auth = config.AuthenticationConfig()
496
user = auth.get_user('ssh', self._host, self._port)
497
client_medium = medium.SmartSSHClientMedium(self._host, self._port,
498
user, self._password, self.base,
499
bzr_remote_path=bzr_remote_path)
500
return client_medium, (user, self._password)
503
class RemoteHTTPTransport(RemoteTransport):
504
"""Just a way to connect between a bzr+http:// url and http://.
506
This connection operates slightly differently than the RemoteSSHTransport.
507
It uses a plain http:// transport underneath, which defines what remote
508
.bzr/smart URL we are connected to. From there, all paths that are sent are
509
sent as relative paths, this way, the remote side can properly
510
de-reference them, since it is likely doing rewrite rules to translate an
511
HTTP path into a local path.
514
def __init__(self, base, _from_transport=None, http_transport=None):
515
if http_transport is None:
516
# FIXME: the password may be lost here because it appears in the
517
# url only for an intial construction (when the url came from the
519
http_url = base[len('bzr+'):]
520
self._http_transport = transport.get_transport(http_url)
522
self._http_transport = http_transport
523
super(RemoteHTTPTransport, self).__init__(
524
base, _from_transport=_from_transport)
526
def _build_medium(self):
527
# We let http_transport take care of the credentials
528
return self._http_transport.get_smart_medium(), None
530
def _remote_path(self, relpath):
531
"""After connecting, HTTP Transport only deals in relative URLs."""
532
# Adjust the relpath based on which URL this smart transport is
534
http_base = urlutils.normalize_url(self.get_smart_medium().base)
535
url = urlutils.join(self.base[len('bzr+'):], relpath)
536
url = urlutils.normalize_url(url)
537
return urlutils.relative_url(http_base, url)
539
def clone(self, relative_url):
540
"""Make a new RemoteHTTPTransport related to me.
542
This is re-implemented rather than using the default
543
RemoteTransport.clone() because we must be careful about the underlying
546
Also, the cloned smart transport will POST to the same .bzr/smart
547
location as this transport (although obviously the relative paths in the
548
smart requests may be different). This is so that the server doesn't
549
have to handle .bzr/smart requests at arbitrary places inside .bzr
550
directories, just at the initial URL the user uses.
553
abs_url = self.abspath(relative_url)
556
return RemoteHTTPTransport(abs_url,
557
_from_transport=self,
558
http_transport=self._http_transport)
561
def get_test_permutations():
562
"""Return (transport, server) permutations for testing."""
563
### We may need a little more test framework support to construct an
564
### appropriate RemoteTransport in the future.
565
from bzrlib.smart import server
566
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]