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,
109
medium = self._shared_connection.connection
112
self._client = client._SmartClient(medium, self.base)
114
self._client = _client
116
def _build_medium(self):
117
"""Create the medium if _from_transport does not provide one.
119
The medium is analogous to the connection for ConnectedTransport: it
120
allows connection sharing.
125
def is_readonly(self):
126
"""Smart server transport can do read/write file operations."""
127
resp = self._call2('Transport.is_readonly')
128
if resp == ('yes', ):
130
elif resp == ('no', ):
132
elif (resp == ('error', "Generic bzr smart protocol error: "
133
"bad request 'Transport.is_readonly'") or
134
resp == ('error', "Generic bzr smart protocol error: "
135
"bad request u'Transport.is_readonly'")):
136
# XXX: nasty hack: servers before 0.16 don't have a
137
# 'Transport.is_readonly' verb, so we do what clients before 0.16
141
self._translate_error(resp)
142
raise errors.UnexpectedSmartServerResponse(resp)
144
def get_smart_client(self):
145
return self._get_connection()
147
def get_smart_medium(self):
148
return self._get_connection()
150
def get_shared_medium(self):
151
return self._get_shared_connection()
153
def _remote_path(self, relpath):
154
"""Returns the Unicode version of the absolute path for relpath."""
155
return self._combine_paths(self._path, relpath)
157
def _call(self, method, *args):
158
resp = self._call2(method, *args)
159
self._translate_error(resp)
161
def _call2(self, method, *args):
162
"""Call a method on the remote server."""
163
return self._client.call(method, *args)
165
def _call_with_body_bytes(self, method, args, body):
166
"""Call a method on the remote server with body bytes."""
167
return self._client.call_with_body_bytes(method, args, body)
169
def has(self, relpath):
170
"""Indicate whether a remote file of the given name exists or not.
172
:see: Transport.has()
174
resp = self._call2('has', self._remote_path(relpath))
175
if resp == ('yes', ):
177
elif resp == ('no', ):
180
self._translate_error(resp)
182
def get(self, relpath):
183
"""Return file-like object reading the contents of a remote file.
185
:see: Transport.get_bytes()/get_file()
187
return StringIO(self.get_bytes(relpath))
189
def get_bytes(self, relpath):
190
remote = self._remote_path(relpath)
191
request = self.get_smart_medium().get_request()
192
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
193
smart_protocol.call('get', remote)
194
resp = smart_protocol.read_response_tuple(True)
196
smart_protocol.cancel_read_body()
197
self._translate_error(resp, relpath)
198
return smart_protocol.read_body_bytes()
200
def _serialise_optional_mode(self, mode):
206
def mkdir(self, relpath, mode=None):
207
resp = self._call2('mkdir', self._remote_path(relpath),
208
self._serialise_optional_mode(mode))
209
self._translate_error(resp)
211
def open_write_stream(self, relpath, mode=None):
212
"""See Transport.open_write_stream."""
213
self.put_bytes(relpath, "", mode)
214
result = transport.AppendBasedFileStream(self, relpath)
215
transport._file_streams[self.abspath(relpath)] = result
218
def put_bytes(self, relpath, upload_contents, mode=None):
219
# FIXME: upload_file is probably not safe for non-ascii characters -
220
# should probably just pass all parameters as length-delimited
222
if type(upload_contents) is unicode:
223
# Although not strictly correct, we raise UnicodeEncodeError to be
224
# compatible with other transports.
225
raise UnicodeEncodeError(
226
'undefined', upload_contents, 0, 1,
227
'put_bytes must be given bytes, not unicode.')
228
resp = self._call_with_body_bytes('put',
229
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
231
self._translate_error(resp)
232
return len(upload_contents)
234
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
235
create_parent_dir=False,
237
"""See Transport.put_bytes_non_atomic."""
238
# FIXME: no encoding in the transport!
239
create_parent_str = 'F'
240
if create_parent_dir:
241
create_parent_str = 'T'
243
resp = self._call_with_body_bytes(
245
(self._remote_path(relpath), self._serialise_optional_mode(mode),
246
create_parent_str, self._serialise_optional_mode(dir_mode)),
248
self._translate_error(resp)
250
def put_file(self, relpath, upload_file, mode=None):
251
# its not ideal to seek back, but currently put_non_atomic_file depends
252
# on transports not reading before failing - which is a faulty
253
# assumption I think - RBC 20060915
254
pos = upload_file.tell()
256
return self.put_bytes(relpath, upload_file.read(), mode)
258
upload_file.seek(pos)
261
def put_file_non_atomic(self, relpath, f, mode=None,
262
create_parent_dir=False,
264
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
265
create_parent_dir=create_parent_dir,
268
def append_file(self, relpath, from_file, mode=None):
269
return self.append_bytes(relpath, from_file.read(), mode)
271
def append_bytes(self, relpath, bytes, mode=None):
272
resp = self._call_with_body_bytes(
274
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
276
if resp[0] == 'appended':
278
self._translate_error(resp)
280
def delete(self, relpath):
281
resp = self._call2('delete', self._remote_path(relpath))
282
self._translate_error(resp)
284
def external_url(self):
285
"""See bzrlib.transport.Transport.external_url."""
286
# the external path for RemoteTransports is the base
289
def recommended_page_size(self):
290
"""Return the recommended page size for this transport."""
293
def _readv(self, relpath, offsets):
297
offsets = list(offsets)
299
sorted_offsets = sorted(offsets)
300
# turn the list of offsets into a stack
301
offset_stack = iter(offsets)
302
cur_offset_and_size = offset_stack.next()
303
coalesced = list(self._coalesce_offsets(sorted_offsets,
304
limit=self._max_readv_combine,
305
fudge_factor=self._bytes_to_read_before_seek))
307
request = self.get_smart_medium().get_request()
308
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
309
smart_protocol.call_with_body_readv_array(
310
('readv', self._remote_path(relpath)),
311
[(c.start, c.length) for c in coalesced])
312
resp = smart_protocol.read_response_tuple(True)
314
if resp[0] != 'readv':
315
# This should raise an exception
316
smart_protocol.cancel_read_body()
317
self._translate_error(resp)
320
# FIXME: this should know how many bytes are needed, for clarity.
321
data = smart_protocol.read_body_bytes()
322
# Cache the results, but only until they have been fulfilled
324
for c_offset in coalesced:
325
if len(data) < c_offset.length:
326
raise errors.ShortReadvError(relpath, c_offset.start,
327
c_offset.length, actual=len(data))
328
for suboffset, subsize in c_offset.ranges:
329
key = (c_offset.start+suboffset, subsize)
330
data_map[key] = data[suboffset:suboffset+subsize]
331
data = data[c_offset.length:]
333
# Now that we've read some data, see if we can yield anything back
334
while cur_offset_and_size in data_map:
335
this_data = data_map.pop(cur_offset_and_size)
336
yield cur_offset_and_size[0], this_data
337
cur_offset_and_size = offset_stack.next()
339
def rename(self, rel_from, rel_to):
341
self._remote_path(rel_from),
342
self._remote_path(rel_to))
344
def move(self, rel_from, rel_to):
346
self._remote_path(rel_from),
347
self._remote_path(rel_to))
349
def rmdir(self, relpath):
350
resp = self._call('rmdir', self._remote_path(relpath))
352
def _translate_error(self, resp, orig_path=None):
353
"""Raise an exception from a response"""
360
elif what == 'NoSuchFile':
361
if orig_path is not None:
362
error_path = orig_path
365
raise errors.NoSuchFile(error_path)
366
elif what == 'error':
367
raise errors.SmartProtocolError(unicode(resp[1]))
368
elif what == 'FileExists':
369
raise errors.FileExists(resp[1])
370
elif what == 'DirectoryNotEmpty':
371
raise errors.DirectoryNotEmpty(resp[1])
372
elif what == 'ShortReadvError':
373
raise errors.ShortReadvError(resp[1], int(resp[2]),
374
int(resp[3]), int(resp[4]))
375
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
376
encoding = str(resp[1]) # encoding must always be a string
380
reason = str(resp[5]) # reason must always be a string
381
if val.startswith('u:'):
382
val = val[2:].decode('utf-8')
383
elif val.startswith('s:'):
384
val = val[2:].decode('base64')
385
if what == 'UnicodeDecodeError':
386
raise UnicodeDecodeError(encoding, val, start, end, reason)
387
elif what == 'UnicodeEncodeError':
388
raise UnicodeEncodeError(encoding, val, start, end, reason)
389
elif what == "ReadOnlyError":
390
raise errors.TransportNotPossible('readonly transport')
391
elif what == "ReadError":
392
if orig_path is not None:
393
error_path = orig_path
396
raise errors.ReadError(error_path)
397
elif what == "PermissionDenied":
398
if orig_path is not None:
399
error_path = orig_path
402
raise errors.PermissionDenied(error_path)
404
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
406
def disconnect(self):
407
self.get_smart_medium().disconnect()
409
def delete_tree(self, relpath):
410
raise errors.TransportNotPossible('readonly transport')
412
def stat(self, relpath):
413
resp = self._call2('stat', self._remote_path(relpath))
414
if resp[0] == 'stat':
415
return _SmartStat(int(resp[1]), int(resp[2], 8))
417
self._translate_error(resp)
419
## def lock_read(self, relpath):
420
## """Lock the given file for shared (read) access.
421
## :return: A lock object, which should be passed to Transport.unlock()
423
## # The old RemoteBranch ignore lock for reading, so we will
424
## # continue that tradition and return a bogus lock object.
425
## class BogusLock(object):
426
## def __init__(self, path):
430
## return BogusLock(relpath)
435
def list_dir(self, relpath):
436
resp = self._call2('list_dir', self._remote_path(relpath))
437
if resp[0] == 'names':
438
return [name.encode('ascii') for name in resp[1:]]
440
self._translate_error(resp)
442
def iter_files_recursive(self):
443
resp = self._call2('iter_files_recursive', self._remote_path(''))
444
if resp[0] == 'names':
447
self._translate_error(resp)
450
class RemoteTCPTransport(RemoteTransport):
451
"""Connection to smart server over plain tcp.
453
This is essentially just a factory to get 'RemoteTransport(url,
454
SmartTCPClientMedium).
457
def _build_medium(self):
458
assert self.base.startswith('bzr://')
459
return medium.SmartTCPClientMedium(self._host, self._port), None
462
class RemoteSSHTransport(RemoteTransport):
463
"""Connection to smart server over SSH.
465
This is essentially just a factory to get 'RemoteTransport(url,
466
SmartSSHClientMedium).
469
def _build_medium(self):
470
assert self.base.startswith('bzr+ssh://')
471
# ssh will prompt the user for a password if needed and if none is
472
# provided but it will not give it back, so no credentials can be
474
location_config = config.LocationConfig(self.base)
475
bzr_remote_path = location_config.get_bzr_remote_path()
476
return medium.SmartSSHClientMedium(self._host, self._port,
477
self._user, self._password, bzr_remote_path=bzr_remote_path), None
480
class RemoteHTTPTransport(RemoteTransport):
481
"""Just a way to connect between a bzr+http:// url and http://.
483
This connection operates slightly differently than the RemoteSSHTransport.
484
It uses a plain http:// transport underneath, which defines what remote
485
.bzr/smart URL we are connected to. From there, all paths that are sent are
486
sent as relative paths, this way, the remote side can properly
487
de-reference them, since it is likely doing rewrite rules to translate an
488
HTTP path into a local path.
491
def __init__(self, base, _from_transport=None, http_transport=None):
492
assert ( base.startswith('bzr+http://') or base.startswith('bzr+https://') )
494
if http_transport is None:
495
# FIXME: the password may be lost here because it appears in the
496
# url only for an intial construction (when the url came from the
498
http_url = base[len('bzr+'):]
499
self._http_transport = transport.get_transport(http_url)
501
self._http_transport = http_transport
502
super(RemoteHTTPTransport, self).__init__(
503
base, _from_transport=_from_transport)
505
def _build_medium(self):
506
# We let http_transport take care of the credentials
507
return self._http_transport.get_smart_medium(), None
509
def _remote_path(self, relpath):
510
"""After connecting, HTTP Transport only deals in relative URLs."""
511
# Adjust the relpath based on which URL this smart transport is
513
http_base = urlutils.normalize_url(self.get_smart_medium().base)
514
url = urlutils.join(self.base[len('bzr+'):], relpath)
515
url = urlutils.normalize_url(url)
516
return urlutils.relative_url(http_base, url)
518
def clone(self, relative_url):
519
"""Make a new RemoteHTTPTransport related to me.
521
This is re-implemented rather than using the default
522
RemoteTransport.clone() because we must be careful about the underlying
525
Also, the cloned smart transport will POST to the same .bzr/smart
526
location as this transport (although obviously the relative paths in the
527
smart requests may be different). This is so that the server doesn't
528
have to handle .bzr/smart requests at arbitrary places inside .bzr
529
directories, just at the initial URL the user uses.
532
abs_url = self.abspath(relative_url)
535
return RemoteHTTPTransport(abs_url,
536
_from_transport=self,
537
http_transport=self._http_transport)
540
def get_test_permutations():
541
"""Return (transport, server) permutations for testing."""
542
### We may need a little more test framework support to construct an
543
### appropriate RemoteTransport in the future.
544
from bzrlib.smart import server
545
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]