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
37
from bzrlib.smart import client, medium, protocol
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
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
65
# responsibilities: Put those on SmartClient or similar. This is vital for
66
# the ability to support multiple versions of the smart protocol over time:
67
# RemoteTransport is an adapter from the Transport object model to the
68
# SmartClient model, not an encoder.
70
# FIXME: the medium parameter should be private, only the tests requires
71
# it. It may be even clearer to define a TestRemoteTransport that handles
72
# the specific cases of providing a _client and/or a _medium, and leave
73
# RemoteTransport as an abstract class.
74
def __init__(self, url, _from_transport=None, medium=None, _client=None):
77
:param _from_transport: Another RemoteTransport instance that this
78
one is being cloned from. Attributes such as the medium will
81
:param medium: The medium to use for this RemoteTransport. This must be
82
supplied if _from_transport is None.
84
:param _client: Override the _SmartClient used by this transport. This
85
should only be used for testing purposes; normally this is
86
determined from the medium.
88
super(RemoteTransport, self).__init__(url,
89
_from_transport=_from_transport)
91
# The medium is the connection, except when we need to share it with
92
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
93
# what we want to share is really the shared connection.
95
if _from_transport is None:
96
# If no _from_transport is specified, we need to intialize the
100
medium, credentials = self._build_medium()
101
if 'hpss' in debug.debug_flags:
102
trace.mutter('hpss: Built a new medium: %s',
103
medium.__class__.__name__)
104
self._shared_connection = transport._SharedConnection(medium,
108
self._client = client._SmartClient(self.get_shared_medium())
110
self._client = _client
112
def _build_medium(self):
113
"""Create the medium if _from_transport does not provide one.
115
The medium is analogous to the connection for ConnectedTransport: it
116
allows connection sharing.
121
def is_readonly(self):
122
"""Smart server transport can do read/write file operations."""
123
resp = self._call2('Transport.is_readonly')
124
if resp == ('yes', ):
126
elif resp == ('no', ):
128
elif (resp == ('error', "Generic bzr smart protocol error: "
129
"bad request 'Transport.is_readonly'") or
130
resp == ('error', "Generic bzr smart protocol error: "
131
"bad request u'Transport.is_readonly'")):
132
# XXX: nasty hack: servers before 0.16 don't have a
133
# 'Transport.is_readonly' verb, so we do what clients before 0.16
137
self._translate_error(resp)
138
raise errors.UnexpectedSmartServerResponse(resp)
140
def get_smart_client(self):
141
return self._get_connection()
143
def get_smart_medium(self):
144
return self._get_connection()
146
def get_shared_medium(self):
147
return self._get_shared_connection()
149
def _remote_path(self, relpath):
150
"""Returns the Unicode version of the absolute path for relpath."""
151
return self._combine_paths(self._path, relpath)
153
def _call(self, method, *args):
154
resp = self._call2(method, *args)
155
self._translate_error(resp)
157
def _call2(self, method, *args):
158
"""Call a method on the remote server."""
159
return self._client.call(method, *args)
161
def _call_with_body_bytes(self, method, args, body):
162
"""Call a method on the remote server with body bytes."""
163
return self._client.call_with_body_bytes(method, args, body)
165
def has(self, relpath):
166
"""Indicate whether a remote file of the given name exists or not.
168
:see: Transport.has()
170
resp = self._call2('has', self._remote_path(relpath))
171
if resp == ('yes', ):
173
elif resp == ('no', ):
176
self._translate_error(resp)
178
def get(self, relpath):
179
"""Return file-like object reading the contents of a remote file.
181
:see: Transport.get_bytes()/get_file()
183
return StringIO(self.get_bytes(relpath))
185
def get_bytes(self, relpath):
186
remote = self._remote_path(relpath)
187
request = self.get_smart_medium().get_request()
188
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
189
smart_protocol.call('get', remote)
190
resp = smart_protocol.read_response_tuple(True)
192
smart_protocol.cancel_read_body()
193
self._translate_error(resp, relpath)
194
return smart_protocol.read_body_bytes()
196
def _serialise_optional_mode(self, mode):
202
def mkdir(self, relpath, mode=None):
203
resp = self._call2('mkdir', self._remote_path(relpath),
204
self._serialise_optional_mode(mode))
205
self._translate_error(resp)
207
def open_write_stream(self, relpath, mode=None):
208
"""See Transport.open_write_stream."""
209
self.put_bytes(relpath, "", mode)
210
result = transport.AppendBasedFileStream(self, relpath)
211
transport._file_streams[self.abspath(relpath)] = result
214
def put_bytes(self, relpath, upload_contents, mode=None):
215
# FIXME: upload_file is probably not safe for non-ascii characters -
216
# should probably just pass all parameters as length-delimited
218
if type(upload_contents) is unicode:
219
# Although not strictly correct, we raise UnicodeEncodeError to be
220
# compatible with other transports.
221
raise UnicodeEncodeError(
222
'undefined', upload_contents, 0, 1,
223
'put_bytes must be given bytes, not unicode.')
224
resp = self._call_with_body_bytes('put',
225
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
227
self._translate_error(resp)
228
return len(upload_contents)
230
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
231
create_parent_dir=False,
233
"""See Transport.put_bytes_non_atomic."""
234
# FIXME: no encoding in the transport!
235
create_parent_str = 'F'
236
if create_parent_dir:
237
create_parent_str = 'T'
239
resp = self._call_with_body_bytes(
241
(self._remote_path(relpath), self._serialise_optional_mode(mode),
242
create_parent_str, self._serialise_optional_mode(dir_mode)),
244
self._translate_error(resp)
246
def put_file(self, relpath, upload_file, mode=None):
247
# its not ideal to seek back, but currently put_non_atomic_file depends
248
# on transports not reading before failing - which is a faulty
249
# assumption I think - RBC 20060915
250
pos = upload_file.tell()
252
return self.put_bytes(relpath, upload_file.read(), mode)
254
upload_file.seek(pos)
257
def put_file_non_atomic(self, relpath, f, mode=None,
258
create_parent_dir=False,
260
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
261
create_parent_dir=create_parent_dir,
264
def append_file(self, relpath, from_file, mode=None):
265
return self.append_bytes(relpath, from_file.read(), mode)
267
def append_bytes(self, relpath, bytes, mode=None):
268
resp = self._call_with_body_bytes(
270
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
272
if resp[0] == 'appended':
274
self._translate_error(resp)
276
def delete(self, relpath):
277
resp = self._call2('delete', self._remote_path(relpath))
278
self._translate_error(resp)
280
def external_url(self):
281
"""See bzrlib.transport.Transport.external_url."""
282
# the external path for RemoteTransports is the base
285
def _readv(self, relpath, offsets):
289
offsets = list(offsets)
291
sorted_offsets = sorted(offsets)
292
# turn the list of offsets into a stack
293
offset_stack = iter(offsets)
294
cur_offset_and_size = offset_stack.next()
295
coalesced = list(self._coalesce_offsets(sorted_offsets,
296
limit=self._max_readv_combine,
297
fudge_factor=self._bytes_to_read_before_seek))
299
request = self.get_smart_medium().get_request()
300
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
301
smart_protocol.call_with_body_readv_array(
302
('readv', self._remote_path(relpath)),
303
[(c.start, c.length) for c in coalesced])
304
resp = smart_protocol.read_response_tuple(True)
306
if resp[0] != 'readv':
307
# This should raise an exception
308
smart_protocol.cancel_read_body()
309
self._translate_error(resp)
312
# FIXME: this should know how many bytes are needed, for clarity.
313
data = smart_protocol.read_body_bytes()
314
# Cache the results, but only until they have been fulfilled
316
for c_offset in coalesced:
317
if len(data) < c_offset.length:
318
raise errors.ShortReadvError(relpath, c_offset.start,
319
c_offset.length, actual=len(data))
320
for suboffset, subsize in c_offset.ranges:
321
key = (c_offset.start+suboffset, subsize)
322
data_map[key] = data[suboffset:suboffset+subsize]
323
data = data[c_offset.length:]
325
# Now that we've read some data, see if we can yield anything back
326
while cur_offset_and_size in data_map:
327
this_data = data_map.pop(cur_offset_and_size)
328
yield cur_offset_and_size[0], this_data
329
cur_offset_and_size = offset_stack.next()
331
def rename(self, rel_from, rel_to):
333
self._remote_path(rel_from),
334
self._remote_path(rel_to))
336
def move(self, rel_from, rel_to):
338
self._remote_path(rel_from),
339
self._remote_path(rel_to))
341
def rmdir(self, relpath):
342
resp = self._call('rmdir', self._remote_path(relpath))
344
def _translate_error(self, resp, orig_path=None):
345
"""Raise an exception from a response"""
352
elif what == 'NoSuchFile':
353
if orig_path is not None:
354
error_path = orig_path
357
raise errors.NoSuchFile(error_path)
358
elif what == 'error':
359
raise errors.SmartProtocolError(unicode(resp[1]))
360
elif what == 'FileExists':
361
raise errors.FileExists(resp[1])
362
elif what == 'DirectoryNotEmpty':
363
raise errors.DirectoryNotEmpty(resp[1])
364
elif what == 'ShortReadvError':
365
raise errors.ShortReadvError(resp[1], int(resp[2]),
366
int(resp[3]), int(resp[4]))
367
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
368
encoding = str(resp[1]) # encoding must always be a string
372
reason = str(resp[5]) # reason must always be a string
373
if val.startswith('u:'):
374
val = val[2:].decode('utf-8')
375
elif val.startswith('s:'):
376
val = val[2:].decode('base64')
377
if what == 'UnicodeDecodeError':
378
raise UnicodeDecodeError(encoding, val, start, end, reason)
379
elif what == 'UnicodeEncodeError':
380
raise UnicodeEncodeError(encoding, val, start, end, reason)
381
elif what == "ReadOnlyError":
382
raise errors.TransportNotPossible('readonly transport')
383
elif what == "ReadError":
384
if orig_path is not None:
385
error_path = orig_path
388
raise errors.ReadError(error_path)
390
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
392
def disconnect(self):
393
self.get_smart_medium().disconnect()
395
def delete_tree(self, relpath):
396
raise errors.TransportNotPossible('readonly transport')
398
def stat(self, relpath):
399
resp = self._call2('stat', self._remote_path(relpath))
400
if resp[0] == 'stat':
401
return _SmartStat(int(resp[1]), int(resp[2], 8))
403
self._translate_error(resp)
405
## def lock_read(self, relpath):
406
## """Lock the given file for shared (read) access.
407
## :return: A lock object, which should be passed to Transport.unlock()
409
## # The old RemoteBranch ignore lock for reading, so we will
410
## # continue that tradition and return a bogus lock object.
411
## class BogusLock(object):
412
## def __init__(self, path):
416
## return BogusLock(relpath)
421
def list_dir(self, relpath):
422
resp = self._call2('list_dir', self._remote_path(relpath))
423
if resp[0] == 'names':
424
return [name.encode('ascii') for name in resp[1:]]
426
self._translate_error(resp)
428
def iter_files_recursive(self):
429
resp = self._call2('iter_files_recursive', self._remote_path(''))
430
if resp[0] == 'names':
433
self._translate_error(resp)
436
class RemoteTCPTransport(RemoteTransport):
437
"""Connection to smart server over plain tcp.
439
This is essentially just a factory to get 'RemoteTransport(url,
440
SmartTCPClientMedium).
443
def _build_medium(self):
444
assert self.base.startswith('bzr://')
445
return medium.SmartTCPClientMedium(self._host, self._port), None
448
class RemoteSSHTransport(RemoteTransport):
449
"""Connection to smart server over SSH.
451
This is essentially just a factory to get 'RemoteTransport(url,
452
SmartSSHClientMedium).
455
def _build_medium(self):
456
assert self.base.startswith('bzr+ssh://')
457
# ssh will prompt the user for a password if needed and if none is
458
# provided but it will not give it back, so no credentials can be
460
location_config = config.LocationConfig(self.base)
461
bzr_remote_path = location_config.get_bzr_remote_path()
462
return medium.SmartSSHClientMedium(self._host, self._port,
463
self._user, self._password, bzr_remote_path=bzr_remote_path), None
466
class RemoteHTTPTransport(RemoteTransport):
467
"""Just a way to connect between a bzr+http:// url and http://.
469
This connection operates slightly differently than the RemoteSSHTransport.
470
It uses a plain http:// transport underneath, which defines what remote
471
.bzr/smart URL we are connected to. From there, all paths that are sent are
472
sent as relative paths, this way, the remote side can properly
473
de-reference them, since it is likely doing rewrite rules to translate an
474
HTTP path into a local path.
477
def __init__(self, base, _from_transport=None, http_transport=None):
478
assert ( base.startswith('bzr+http://') or base.startswith('bzr+https://') )
480
if http_transport is None:
481
# FIXME: the password may be lost here because it appears in the
482
# url only for an intial construction (when the url came from the
484
http_url = base[len('bzr+'):]
485
self._http_transport = transport.get_transport(http_url)
487
self._http_transport = http_transport
488
super(RemoteHTTPTransport, self).__init__(
489
base, _from_transport=_from_transport)
491
def _build_medium(self):
492
# We let http_transport take care of the credentials
493
return self._http_transport.get_smart_medium(), None
495
def _remote_path(self, relpath):
496
"""After connecting, HTTP Transport only deals in relative URLs."""
497
# Adjust the relpath based on which URL this smart transport is
499
http_base = urlutils.normalize_url(self._http_transport.base)
500
url = urlutils.join(self.base[len('bzr+'):], relpath)
501
url = urlutils.normalize_url(url)
502
return urlutils.relative_url(http_base, url)
504
def clone(self, relative_url):
505
"""Make a new RemoteHTTPTransport related to me.
507
This is re-implemented rather than using the default
508
RemoteTransport.clone() because we must be careful about the underlying
511
Also, the cloned smart transport will POST to the same .bzr/smart
512
location as this transport (although obviously the relative paths in the
513
smart requests may be different). This is so that the server doesn't
514
have to handle .bzr/smart requests at arbitrary places inside .bzr
515
directories, just at the initial URL the user uses.
517
The exception is parent paths (i.e. relative_url of "..").
520
abs_url = self.abspath(relative_url)
523
# We either use the exact same http_transport (for child locations), or
524
# a clone of the underlying http_transport (for parent locations). This
525
# means we share the connection.
526
norm_base = urlutils.normalize_url(self.base)
527
norm_abs_url = urlutils.normalize_url(abs_url)
528
normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
529
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
530
http_transport = self._http_transport.clone(normalized_rel_url)
532
http_transport = self._http_transport
533
return RemoteHTTPTransport(abs_url,
534
_from_transport=self,
535
http_transport=http_transport)
538
def get_test_permutations():
539
"""Return (transport, server) permutations for testing."""
540
### We may need a little more test framework support to construct an
541
### appropriate RemoteTransport in the future.
542
from bzrlib.smart import server
543
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]