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
35
from bzrlib.smart import client, medium
36
from bzrlib.symbol_versioning import (deprecated_method, one_four)
39
class _SmartStat(object):
41
def __init__(self, size, mode):
46
class RemoteTransport(transport.ConnectedTransport):
47
"""Connection to a smart server.
49
The connection holds references to the medium that can be used to send
50
requests to the server.
52
The connection has a notion of the current directory to which it's
53
connected; this is incorporated in filenames passed to the server.
55
This supports some higher-level RPC operations and can also be treated
56
like a Transport to do file-like operations.
58
The connection can be made over a tcp socket, an ssh pipe or a series of
59
http requests. There are concrete subclasses for each type:
60
RemoteTCPTransport, etc.
63
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
64
# responsibilities: Put those on SmartClient or similar. This is vital for
65
# the ability to support multiple versions of the smart protocol over time:
66
# RemoteTransport is an adapter from the Transport object model to the
67
# SmartClient model, not an encoder.
69
# FIXME: the medium parameter should be private, only the tests requires
70
# it. It may be even clearer to define a TestRemoteTransport that handles
71
# the specific cases of providing a _client and/or a _medium, and leave
72
# RemoteTransport as an abstract class.
73
def __init__(self, url, _from_transport=None, medium=None, _client=None):
76
:param _from_transport: Another RemoteTransport instance that this
77
one is being cloned from. Attributes such as the medium will
80
:param medium: The medium to use for this RemoteTransport. This must be
81
supplied if _from_transport is None.
83
:param _client: Override the _SmartClient used by this transport. This
84
should only be used for testing purposes; normally this is
85
determined from the medium.
87
super(RemoteTransport, self).__init__(url,
88
_from_transport=_from_transport)
90
# The medium is the connection, except when we need to share it with
91
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
92
# what we want to share is really the shared connection.
94
if _from_transport is None:
95
# If no _from_transport is specified, we need to intialize the
99
medium, credentials = self._build_medium()
100
if 'hpss' in debug.debug_flags:
101
trace.mutter('hpss: Built a new medium: %s',
102
medium.__class__.__name__)
103
self._shared_connection = transport._SharedConnection(medium,
108
# No medium was specified, so share the medium from the
110
medium = self._shared_connection.connection
113
self._client = client._SmartClient(medium, self.base)
115
self._client = _client
117
def _build_medium(self):
118
"""Create the medium if _from_transport does not provide one.
120
The medium is analogous to the connection for ConnectedTransport: it
121
allows connection sharing.
126
def is_readonly(self):
127
"""Smart server transport can do read/write file operations."""
129
resp = self._call2('Transport.is_readonly')
130
except errors.UnknownSmartMethod:
131
# XXX: nasty hack: servers before 0.16 don't have a
132
# 'Transport.is_readonly' verb, so we do what clients before 0.16
135
if resp == ('yes', ):
137
elif resp == ('no', ):
140
self._translate_error(resp)
141
raise errors.UnexpectedSmartServerResponse(resp)
143
def get_smart_client(self):
144
return self._get_connection()
146
def get_smart_medium(self):
147
return self._get_connection()
149
@deprecated_method(one_four)
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):
159
resp = self._call2(method, *args)
160
except errors.ErrorFromSmartServer, err:
161
self._translate_error(err.error_tuple)
162
self._translate_error(resp)
164
def _call2(self, method, *args):
165
"""Call a method on the remote server."""
167
return self._client.call(method, *args)
168
except errors.ErrorFromSmartServer, err:
169
self._translate_error(err.error_tuple)
171
def _call_with_body_bytes(self, method, args, body):
172
"""Call a method on the remote server with body bytes."""
174
return self._client.call_with_body_bytes(method, args, body)
175
except errors.ErrorFromSmartServer, err:
176
self._translate_error(err.error_tuple)
178
def has(self, relpath):
179
"""Indicate whether a remote file of the given name exists or not.
181
:see: Transport.has()
183
resp = self._call2('has', self._remote_path(relpath))
184
if resp == ('yes', ):
186
elif resp == ('no', ):
189
self._translate_error(resp)
191
def get(self, relpath):
192
"""Return file-like object reading the contents of a remote file.
194
:see: Transport.get_bytes()/get_file()
196
return StringIO(self.get_bytes(relpath))
198
def get_bytes(self, relpath):
199
remote = self._remote_path(relpath)
201
resp, response_handler = self._client.call_expecting_body('get', remote)
202
except errors.ErrorFromSmartServer, err:
203
self._translate_error(err.error_tuple, relpath)
205
response_handler.cancel_read_body()
206
raise errors.UnexpectedSmartServerResponse(resp)
207
return response_handler.read_body_bytes()
209
def _serialise_optional_mode(self, mode):
215
def mkdir(self, relpath, mode=None):
216
resp = self._call2('mkdir', self._remote_path(relpath),
217
self._serialise_optional_mode(mode))
218
self._translate_error(resp)
220
def open_write_stream(self, relpath, mode=None):
221
"""See Transport.open_write_stream."""
222
self.put_bytes(relpath, "", mode)
223
result = transport.AppendBasedFileStream(self, relpath)
224
transport._file_streams[self.abspath(relpath)] = result
227
def put_bytes(self, relpath, upload_contents, mode=None):
228
# FIXME: upload_file is probably not safe for non-ascii characters -
229
# should probably just pass all parameters as length-delimited
231
if type(upload_contents) is unicode:
232
# Although not strictly correct, we raise UnicodeEncodeError to be
233
# compatible with other transports.
234
raise UnicodeEncodeError(
235
'undefined', upload_contents, 0, 1,
236
'put_bytes must be given bytes, not unicode.')
237
resp = self._call_with_body_bytes('put',
238
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
240
self._translate_error(resp)
241
return len(upload_contents)
243
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
244
create_parent_dir=False,
246
"""See Transport.put_bytes_non_atomic."""
247
# FIXME: no encoding in the transport!
248
create_parent_str = 'F'
249
if create_parent_dir:
250
create_parent_str = 'T'
252
resp = self._call_with_body_bytes(
254
(self._remote_path(relpath), self._serialise_optional_mode(mode),
255
create_parent_str, self._serialise_optional_mode(dir_mode)),
257
self._translate_error(resp)
259
def put_file(self, relpath, upload_file, mode=None):
260
# its not ideal to seek back, but currently put_non_atomic_file depends
261
# on transports not reading before failing - which is a faulty
262
# assumption I think - RBC 20060915
263
pos = upload_file.tell()
265
return self.put_bytes(relpath, upload_file.read(), mode)
267
upload_file.seek(pos)
270
def put_file_non_atomic(self, relpath, f, mode=None,
271
create_parent_dir=False,
273
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
274
create_parent_dir=create_parent_dir,
277
def append_file(self, relpath, from_file, mode=None):
278
return self.append_bytes(relpath, from_file.read(), mode)
280
def append_bytes(self, relpath, bytes, mode=None):
281
resp = self._call_with_body_bytes(
283
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
285
if resp[0] == 'appended':
287
self._translate_error(resp)
289
def delete(self, relpath):
290
resp = self._call2('delete', self._remote_path(relpath))
291
self._translate_error(resp)
293
def external_url(self):
294
"""See bzrlib.transport.Transport.external_url."""
295
# the external path for RemoteTransports is the base
298
def recommended_page_size(self):
299
"""Return the recommended page size for this transport."""
302
def _readv(self, relpath, offsets):
306
offsets = list(offsets)
308
sorted_offsets = sorted(offsets)
309
# turn the list of offsets into a stack
310
offset_stack = iter(offsets)
311
cur_offset_and_size = offset_stack.next()
312
coalesced = list(self._coalesce_offsets(sorted_offsets,
313
limit=self._max_readv_combine,
314
fudge_factor=self._bytes_to_read_before_seek))
317
result = self._client.call_with_body_readv_array(
318
('readv', self._remote_path(relpath),),
319
[(c.start, c.length) for c in coalesced])
320
resp, response_handler = result
321
except errors.ErrorFromSmartServer, err:
322
self._translate_error(err.error_tuple)
324
if resp[0] != 'readv':
325
# This should raise an exception
326
response_handler.cancel_read_body()
327
raise errors.UnexpectedSmartServerResponse(resp)
329
# FIXME: this should know how many bytes are needed, for clarity.
330
data = response_handler.read_body_bytes()
331
# Cache the results, but only until they have been fulfilled
333
for c_offset in coalesced:
334
if len(data) < c_offset.length:
335
raise errors.ShortReadvError(relpath, c_offset.start,
336
c_offset.length, actual=len(data))
337
for suboffset, subsize in c_offset.ranges:
338
key = (c_offset.start+suboffset, subsize)
339
data_map[key] = data[suboffset:suboffset+subsize]
340
data = data[c_offset.length:]
342
# Now that we've read some data, see if we can yield anything back
343
while cur_offset_and_size in data_map:
344
this_data = data_map.pop(cur_offset_and_size)
345
yield cur_offset_and_size[0], this_data
346
cur_offset_and_size = offset_stack.next()
348
def rename(self, rel_from, rel_to):
350
self._remote_path(rel_from),
351
self._remote_path(rel_to))
353
def move(self, rel_from, rel_to):
355
self._remote_path(rel_from),
356
self._remote_path(rel_to))
358
def rmdir(self, relpath):
359
resp = self._call('rmdir', self._remote_path(relpath))
361
def _translate_error(self, resp, orig_path=None):
362
"""Raise an exception from a response"""
369
elif what == 'NoSuchFile':
370
if orig_path is not None:
371
error_path = orig_path
374
raise errors.NoSuchFile(error_path)
375
elif what == 'error':
376
raise errors.SmartProtocolError(unicode(resp[1]))
377
elif what == 'FileExists':
378
raise errors.FileExists(resp[1])
379
elif what == 'DirectoryNotEmpty':
380
raise errors.DirectoryNotEmpty(resp[1])
381
elif what == 'ShortReadvError':
382
raise errors.ShortReadvError(resp[1], int(resp[2]),
383
int(resp[3]), int(resp[4]))
384
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
385
encoding = str(resp[1]) # encoding must always be a string
389
reason = str(resp[5]) # reason must always be a string
390
if val.startswith('u:'):
391
val = val[2:].decode('utf-8')
392
elif val.startswith('s:'):
393
val = val[2:].decode('base64')
394
if what == 'UnicodeDecodeError':
395
raise UnicodeDecodeError(encoding, val, start, end, reason)
396
elif what == 'UnicodeEncodeError':
397
raise UnicodeEncodeError(encoding, val, start, end, reason)
398
elif what == "ReadOnlyError":
399
raise errors.TransportNotPossible('readonly transport')
400
elif what == "ReadError":
401
if orig_path is not None:
402
error_path = orig_path
405
raise errors.ReadError(error_path)
406
elif what == "PermissionDenied":
407
if orig_path is not None:
408
error_path = orig_path
411
raise errors.PermissionDenied(error_path)
413
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
415
def disconnect(self):
416
self.get_smart_medium().disconnect()
418
def delete_tree(self, relpath):
419
raise errors.TransportNotPossible('readonly transport')
421
def stat(self, relpath):
422
resp = self._call2('stat', self._remote_path(relpath))
423
if resp[0] == 'stat':
424
return _SmartStat(int(resp[1]), int(resp[2], 8))
426
self._translate_error(resp)
428
## def lock_read(self, relpath):
429
## """Lock the given file for shared (read) access.
430
## :return: A lock object, which should be passed to Transport.unlock()
432
## # The old RemoteBranch ignore lock for reading, so we will
433
## # continue that tradition and return a bogus lock object.
434
## class BogusLock(object):
435
## def __init__(self, path):
439
## return BogusLock(relpath)
444
def list_dir(self, relpath):
445
resp = self._call2('list_dir', self._remote_path(relpath))
446
if resp[0] == 'names':
447
return [name.encode('ascii') for name in resp[1:]]
449
self._translate_error(resp)
451
def iter_files_recursive(self):
452
resp = self._call2('iter_files_recursive', self._remote_path(''))
453
if resp[0] == 'names':
456
self._translate_error(resp)
459
class RemoteTCPTransport(RemoteTransport):
460
"""Connection to smart server over plain tcp.
462
This is essentially just a factory to get 'RemoteTransport(url,
463
SmartTCPClientMedium).
466
def _build_medium(self):
467
return medium.SmartTCPClientMedium(self._host, self._port), None
470
class RemoteSSHTransport(RemoteTransport):
471
"""Connection to smart server over SSH.
473
This is essentially just a factory to get 'RemoteTransport(url,
474
SmartSSHClientMedium).
477
def _build_medium(self):
478
# ssh will prompt the user for a password if needed and if none is
479
# provided but it will not give it back, so no credentials can be
481
location_config = config.LocationConfig(self.base)
482
bzr_remote_path = location_config.get_bzr_remote_path()
483
return medium.SmartSSHClientMedium(self._host, self._port,
484
self._user, self._password, bzr_remote_path=bzr_remote_path), None
487
class RemoteHTTPTransport(RemoteTransport):
488
"""Just a way to connect between a bzr+http:// url and http://.
490
This connection operates slightly differently than the RemoteSSHTransport.
491
It uses a plain http:// transport underneath, which defines what remote
492
.bzr/smart URL we are connected to. From there, all paths that are sent are
493
sent as relative paths, this way, the remote side can properly
494
de-reference them, since it is likely doing rewrite rules to translate an
495
HTTP path into a local path.
498
def __init__(self, base, _from_transport=None, http_transport=None):
499
if http_transport is None:
500
# FIXME: the password may be lost here because it appears in the
501
# url only for an intial construction (when the url came from the
503
http_url = base[len('bzr+'):]
504
self._http_transport = transport.get_transport(http_url)
506
self._http_transport = http_transport
507
super(RemoteHTTPTransport, self).__init__(
508
base, _from_transport=_from_transport)
510
def _build_medium(self):
511
# We let http_transport take care of the credentials
512
return self._http_transport.get_smart_medium(), None
514
def _remote_path(self, relpath):
515
"""After connecting, HTTP Transport only deals in relative URLs."""
516
# Adjust the relpath based on which URL this smart transport is
518
http_base = urlutils.normalize_url(self.get_smart_medium().base)
519
url = urlutils.join(self.base[len('bzr+'):], relpath)
520
url = urlutils.normalize_url(url)
521
return urlutils.relative_url(http_base, url)
523
def clone(self, relative_url):
524
"""Make a new RemoteHTTPTransport related to me.
526
This is re-implemented rather than using the default
527
RemoteTransport.clone() because we must be careful about the underlying
530
Also, the cloned smart transport will POST to the same .bzr/smart
531
location as this transport (although obviously the relative paths in the
532
smart requests may be different). This is so that the server doesn't
533
have to handle .bzr/smart requests at arbitrary places inside .bzr
534
directories, just at the initial URL the user uses.
537
abs_url = self.abspath(relative_url)
540
return RemoteHTTPTransport(abs_url,
541
_from_transport=self,
542
http_transport=self._http_transport)
545
def get_test_permutations():
546
"""Return (transport, server) permutations for testing."""
547
### We may need a little more test framework support to construct an
548
### appropriate RemoteTransport in the future.
549
from bzrlib.smart import server
550
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]