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
38
from bzrlib.symbol_versioning import (deprecated_method, one_four)
41
class _SmartStat(object):
43
def __init__(self, size, mode):
48
class RemoteTransport(transport.ConnectedTransport):
49
"""Connection to a smart server.
51
The connection holds references to the medium that can be used to send
52
requests to the server.
54
The connection has a notion of the current directory to which it's
55
connected; this is incorporated in filenames passed to the server.
57
This supports some higher-level RPC operations and can also be treated
58
like a Transport to do file-like operations.
60
The connection can be made over a tcp socket, an ssh pipe or a series of
61
http requests. There are concrete subclasses for each type:
62
RemoteTCPTransport, etc.
65
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
66
# responsibilities: Put those on SmartClient or similar. This is vital for
67
# the ability to support multiple versions of the smart protocol over time:
68
# RemoteTransport is an adapter from the Transport object model to the
69
# SmartClient model, not an encoder.
71
# FIXME: the medium parameter should be private, only the tests requires
72
# it. It may be even clearer to define a TestRemoteTransport that handles
73
# the specific cases of providing a _client and/or a _medium, and leave
74
# RemoteTransport as an abstract class.
75
def __init__(self, url, _from_transport=None, medium=None, _client=None):
78
:param _from_transport: Another RemoteTransport instance that this
79
one is being cloned from. Attributes such as the medium will
82
:param medium: The medium to use for this RemoteTransport. This must be
83
supplied if _from_transport is None.
85
:param _client: Override the _SmartClient used by this transport. This
86
should only be used for testing purposes; normally this is
87
determined from the medium.
89
super(RemoteTransport, self).__init__(url,
90
_from_transport=_from_transport)
92
# The medium is the connection, except when we need to share it with
93
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
94
# what we want to share is really the shared connection.
96
if _from_transport is None:
97
# If no _from_transport is specified, we need to intialize the
101
medium, credentials = self._build_medium()
102
if 'hpss' in debug.debug_flags:
103
trace.mutter('hpss: Built a new medium: %s',
104
medium.__class__.__name__)
105
self._shared_connection = transport._SharedConnection(medium,
110
# No medium was specified, so share the medium from the
112
medium = self._shared_connection.connection
115
self._client = client._SmartClient(medium, self.base)
117
self._client = _client
119
def _build_medium(self):
120
"""Create the medium if _from_transport does not provide one.
122
The medium is analogous to the connection for ConnectedTransport: it
123
allows connection sharing.
128
def is_readonly(self):
129
"""Smart server transport can do read/write file operations."""
131
resp = self._call2('Transport.is_readonly')
132
except errors.UnknownSmartMethod:
133
# XXX: nasty hack: servers before 0.16 don't have a
134
# 'Transport.is_readonly' verb, so we do what clients before 0.16
137
if resp == ('yes', ):
139
elif resp == ('no', ):
142
self._translate_error(resp)
143
raise errors.UnexpectedSmartServerResponse(resp)
145
def get_smart_client(self):
146
return self._get_connection()
148
def get_smart_medium(self):
149
return self._get_connection()
151
@deprecated_method(one_four)
152
def get_shared_medium(self):
153
return self._get_shared_connection()
155
def _remote_path(self, relpath):
156
"""Returns the Unicode version of the absolute path for relpath."""
157
return self._combine_paths(self._path, relpath)
159
def _call(self, method, *args):
160
resp = self._call2(method, *args)
161
self._translate_error(resp)
163
def _call2(self, method, *args):
164
"""Call a method on the remote server."""
165
return self._client.call(method, *args)
167
def _call_with_body_bytes(self, method, args, body):
168
"""Call a method on the remote server with body bytes."""
169
return self._client.call_with_body_bytes(method, args, body)
171
def has(self, relpath):
172
"""Indicate whether a remote file of the given name exists or not.
174
:see: Transport.has()
176
resp = self._call2('has', self._remote_path(relpath))
177
if resp == ('yes', ):
179
elif resp == ('no', ):
182
self._translate_error(resp)
184
def get(self, relpath):
185
"""Return file-like object reading the contents of a remote file.
187
:see: Transport.get_bytes()/get_file()
189
return StringIO(self.get_bytes(relpath))
191
def get_bytes(self, relpath):
192
remote = self._remote_path(relpath)
193
request = self.get_smart_medium().get_request()
194
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
195
smart_protocol.call('get', remote)
196
resp = smart_protocol.read_response_tuple(True)
198
smart_protocol.cancel_read_body()
199
self._translate_error(resp, relpath)
200
return smart_protocol.read_body_bytes()
202
def _serialise_optional_mode(self, mode):
208
def mkdir(self, relpath, mode=None):
209
resp = self._call2('mkdir', self._remote_path(relpath),
210
self._serialise_optional_mode(mode))
211
self._translate_error(resp)
213
def open_write_stream(self, relpath, mode=None):
214
"""See Transport.open_write_stream."""
215
self.put_bytes(relpath, "", mode)
216
result = transport.AppendBasedFileStream(self, relpath)
217
transport._file_streams[self.abspath(relpath)] = result
220
def put_bytes(self, relpath, upload_contents, mode=None):
221
# FIXME: upload_file is probably not safe for non-ascii characters -
222
# should probably just pass all parameters as length-delimited
224
if type(upload_contents) is unicode:
225
# Although not strictly correct, we raise UnicodeEncodeError to be
226
# compatible with other transports.
227
raise UnicodeEncodeError(
228
'undefined', upload_contents, 0, 1,
229
'put_bytes must be given bytes, not unicode.')
230
resp = self._call_with_body_bytes('put',
231
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
233
self._translate_error(resp)
234
return len(upload_contents)
236
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
237
create_parent_dir=False,
239
"""See Transport.put_bytes_non_atomic."""
240
# FIXME: no encoding in the transport!
241
create_parent_str = 'F'
242
if create_parent_dir:
243
create_parent_str = 'T'
245
resp = self._call_with_body_bytes(
247
(self._remote_path(relpath), self._serialise_optional_mode(mode),
248
create_parent_str, self._serialise_optional_mode(dir_mode)),
250
self._translate_error(resp)
252
def put_file(self, relpath, upload_file, mode=None):
253
# its not ideal to seek back, but currently put_non_atomic_file depends
254
# on transports not reading before failing - which is a faulty
255
# assumption I think - RBC 20060915
256
pos = upload_file.tell()
258
return self.put_bytes(relpath, upload_file.read(), mode)
260
upload_file.seek(pos)
263
def put_file_non_atomic(self, relpath, f, mode=None,
264
create_parent_dir=False,
266
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
267
create_parent_dir=create_parent_dir,
270
def append_file(self, relpath, from_file, mode=None):
271
return self.append_bytes(relpath, from_file.read(), mode)
273
def append_bytes(self, relpath, bytes, mode=None):
274
resp = self._call_with_body_bytes(
276
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
278
if resp[0] == 'appended':
280
self._translate_error(resp)
282
def delete(self, relpath):
283
resp = self._call2('delete', self._remote_path(relpath))
284
self._translate_error(resp)
286
def external_url(self):
287
"""See bzrlib.transport.Transport.external_url."""
288
# the external path for RemoteTransports is the base
291
def recommended_page_size(self):
292
"""Return the recommended page size for this transport."""
295
def _readv(self, relpath, offsets):
299
offsets = list(offsets)
301
sorted_offsets = sorted(offsets)
302
# turn the list of offsets into a stack
303
offset_stack = iter(offsets)
304
cur_offset_and_size = offset_stack.next()
305
coalesced = list(self._coalesce_offsets(sorted_offsets,
306
limit=self._max_readv_combine,
307
fudge_factor=self._bytes_to_read_before_seek))
309
request = self.get_smart_medium().get_request()
310
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
311
smart_protocol.call_with_body_readv_array(
312
('readv', self._remote_path(relpath)),
313
[(c.start, c.length) for c in coalesced])
314
resp = smart_protocol.read_response_tuple(True)
316
if resp[0] != 'readv':
317
# This should raise an exception
318
smart_protocol.cancel_read_body()
319
self._translate_error(resp)
322
# FIXME: this should know how many bytes are needed, for clarity.
323
data = smart_protocol.read_body_bytes()
324
# Cache the results, but only until they have been fulfilled
326
for c_offset in coalesced:
327
if len(data) < c_offset.length:
328
raise errors.ShortReadvError(relpath, c_offset.start,
329
c_offset.length, actual=len(data))
330
for suboffset, subsize in c_offset.ranges:
331
key = (c_offset.start+suboffset, subsize)
332
data_map[key] = data[suboffset:suboffset+subsize]
333
data = data[c_offset.length:]
335
# Now that we've read some data, see if we can yield anything back
336
while cur_offset_and_size in data_map:
337
this_data = data_map.pop(cur_offset_and_size)
338
yield cur_offset_and_size[0], this_data
339
cur_offset_and_size = offset_stack.next()
341
def rename(self, rel_from, rel_to):
343
self._remote_path(rel_from),
344
self._remote_path(rel_to))
346
def move(self, rel_from, rel_to):
348
self._remote_path(rel_from),
349
self._remote_path(rel_to))
351
def rmdir(self, relpath):
352
resp = self._call('rmdir', self._remote_path(relpath))
354
def _translate_error(self, resp, orig_path=None):
355
"""Raise an exception from a response"""
362
elif what == 'NoSuchFile':
363
if orig_path is not None:
364
error_path = orig_path
367
raise errors.NoSuchFile(error_path)
368
elif what == 'error':
369
raise errors.SmartProtocolError(unicode(resp[1]))
370
elif what == 'FileExists':
371
raise errors.FileExists(resp[1])
372
elif what == 'DirectoryNotEmpty':
373
raise errors.DirectoryNotEmpty(resp[1])
374
elif what == 'ShortReadvError':
375
raise errors.ShortReadvError(resp[1], int(resp[2]),
376
int(resp[3]), int(resp[4]))
377
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
378
encoding = str(resp[1]) # encoding must always be a string
382
reason = str(resp[5]) # reason must always be a string
383
if val.startswith('u:'):
384
val = val[2:].decode('utf-8')
385
elif val.startswith('s:'):
386
val = val[2:].decode('base64')
387
if what == 'UnicodeDecodeError':
388
raise UnicodeDecodeError(encoding, val, start, end, reason)
389
elif what == 'UnicodeEncodeError':
390
raise UnicodeEncodeError(encoding, val, start, end, reason)
391
elif what == "ReadOnlyError":
392
raise errors.TransportNotPossible('readonly transport')
393
elif what == "ReadError":
394
if orig_path is not None:
395
error_path = orig_path
398
raise errors.ReadError(error_path)
399
elif what == "PermissionDenied":
400
if orig_path is not None:
401
error_path = orig_path
404
raise errors.PermissionDenied(error_path)
406
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
408
def disconnect(self):
409
self.get_smart_medium().disconnect()
411
def delete_tree(self, relpath):
412
raise errors.TransportNotPossible('readonly transport')
414
def stat(self, relpath):
415
resp = self._call2('stat', self._remote_path(relpath))
416
if resp[0] == 'stat':
417
return _SmartStat(int(resp[1]), int(resp[2], 8))
419
self._translate_error(resp)
421
## def lock_read(self, relpath):
422
## """Lock the given file for shared (read) access.
423
## :return: A lock object, which should be passed to Transport.unlock()
425
## # The old RemoteBranch ignore lock for reading, so we will
426
## # continue that tradition and return a bogus lock object.
427
## class BogusLock(object):
428
## def __init__(self, path):
432
## return BogusLock(relpath)
437
def list_dir(self, relpath):
438
resp = self._call2('list_dir', self._remote_path(relpath))
439
if resp[0] == 'names':
440
return [name.encode('ascii') for name in resp[1:]]
442
self._translate_error(resp)
444
def iter_files_recursive(self):
445
resp = self._call2('iter_files_recursive', self._remote_path(''))
446
if resp[0] == 'names':
449
self._translate_error(resp)
452
class RemoteTCPTransport(RemoteTransport):
453
"""Connection to smart server over plain tcp.
455
This is essentially just a factory to get 'RemoteTransport(url,
456
SmartTCPClientMedium).
459
def _build_medium(self):
460
assert self.base.startswith('bzr://')
461
return medium.SmartTCPClientMedium(self._host, self._port), None
464
class RemoteSSHTransport(RemoteTransport):
465
"""Connection to smart server over SSH.
467
This is essentially just a factory to get 'RemoteTransport(url,
468
SmartSSHClientMedium).
471
def _build_medium(self):
472
assert self.base.startswith('bzr+ssh://')
473
# ssh will prompt the user for a password if needed and if none is
474
# provided but it will not give it back, so no credentials can be
476
location_config = config.LocationConfig(self.base)
477
bzr_remote_path = location_config.get_bzr_remote_path()
478
return medium.SmartSSHClientMedium(self._host, self._port,
479
self._user, self._password, bzr_remote_path=bzr_remote_path), None
482
class RemoteHTTPTransport(RemoteTransport):
483
"""Just a way to connect between a bzr+http:// url and http://.
485
This connection operates slightly differently than the RemoteSSHTransport.
486
It uses a plain http:// transport underneath, which defines what remote
487
.bzr/smart URL we are connected to. From there, all paths that are sent are
488
sent as relative paths, this way, the remote side can properly
489
de-reference them, since it is likely doing rewrite rules to translate an
490
HTTP path into a local path.
493
def __init__(self, base, _from_transport=None, http_transport=None):
494
assert ( base.startswith('bzr+http://') or base.startswith('bzr+https://') )
496
if http_transport is None:
497
# FIXME: the password may be lost here because it appears in the
498
# url only for an intial construction (when the url came from the
500
http_url = base[len('bzr+'):]
501
self._http_transport = transport.get_transport(http_url)
503
self._http_transport = http_transport
504
super(RemoteHTTPTransport, self).__init__(
505
base, _from_transport=_from_transport)
507
def _build_medium(self):
508
# We let http_transport take care of the credentials
509
return self._http_transport.get_smart_medium(), None
511
def _remote_path(self, relpath):
512
"""After connecting, HTTP Transport only deals in relative URLs."""
513
# Adjust the relpath based on which URL this smart transport is
515
http_base = urlutils.normalize_url(self.get_smart_medium().base)
516
url = urlutils.join(self.base[len('bzr+'):], relpath)
517
url = urlutils.normalize_url(url)
518
return urlutils.relative_url(http_base, url)
520
def clone(self, relative_url):
521
"""Make a new RemoteHTTPTransport related to me.
523
This is re-implemented rather than using the default
524
RemoteTransport.clone() because we must be careful about the underlying
527
Also, the cloned smart transport will POST to the same .bzr/smart
528
location as this transport (although obviously the relative paths in the
529
smart requests may be different). This is so that the server doesn't
530
have to handle .bzr/smart requests at arbitrary places inside .bzr
531
directories, just at the initial URL the user uses.
534
abs_url = self.abspath(relative_url)
537
return RemoteHTTPTransport(abs_url,
538
_from_transport=self,
539
http_transport=self._http_transport)
542
def get_test_permutations():
543
"""Return (transport, server) permutations for testing."""
544
### We may need a little more test framework support to construct an
545
### appropriate RemoteTransport in the future.
546
from bzrlib.smart import server
547
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]