14
14
# along with this program; if not, write to the Free Software
15
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']
17
25
from cStringIO import StringIO
21
29
from bzrlib import (
25
from bzrlib.smart.protocol import SmartClientRequestProtocolOne
26
from bzrlib.smart.medium import SmartTCPClientMedium, SmartSSHClientMedium
34
from bzrlib.smart import client, medium, protocol
28
36
# must do this otherwise urllib can't parse the urls properly :(
29
37
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
34
42
# Port 4155 is the default port for bzr://, registered with IANA.
43
BZR_DEFAULT_INTERFACE = '0.0.0.0'
35
44
BZR_DEFAULT_PORT = 4155
38
class SmartStat(object):
47
class _SmartStat(object):
40
49
def __init__(self, size, mode):
41
50
self.st_size = size
42
51
self.st_mode = mode
45
class SmartTransport(transport.Transport):
54
class RemoteTransport(transport.Transport):
46
55
"""Connection to a smart server.
48
The connection holds references to pipes that can be used to send requests
57
The connection holds references to the medium that can be used to send
58
requests to the server.
51
60
The connection has a notion of the current directory to which it's
52
61
connected; this is incorporated in filenames passed to the server.
54
63
This supports some higher-level RPC operations and can also be treated
55
64
like a Transport to do file-like operations.
57
The connection can be made over a tcp socket, or (in future) an ssh pipe
58
or a series of http requests. There are concrete subclasses for each
59
type: SmartTCPTransport, etc.
66
The connection can be made over a tcp socket, an ssh pipe or a series of
67
http requests. There are concrete subclasses for each type:
68
RemoteTCPTransport, etc.
62
# IMPORTANT FOR IMPLEMENTORS: SmartTransport MUST NOT be given encoding
71
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
63
72
# responsibilities: Put those on SmartClient or similar. This is vital for
64
73
# the ability to support multiple versions of the smart protocol over time:
65
# SmartTransport is an adapter from the Transport object model to the
74
# RemoteTransport is an adapter from the Transport object model to the
66
75
# SmartClient model, not an encoder.
68
77
def __init__(self, url, clone_from=None, medium=None):
76
85
### initialisation order things would blow up.
77
86
if not url.endswith('/'):
79
super(SmartTransport, self).__init__(url)
88
super(RemoteTransport, self).__init__(url)
80
89
self._scheme, self._username, self._password, self._host, self._port, self._path = \
81
90
transport.split_url(url)
82
91
if clone_from is None:
98
107
return self._unparse_url(self._remote_path(relpath))
100
109
def clone(self, relative_url):
101
"""Make a new SmartTransport related to me, sharing the same connection.
110
"""Make a new RemoteTransport related to me, sharing the same connection.
103
112
This essentially opens a handle on a different remote directory.
105
114
if relative_url is None:
106
return SmartTransport(self.base, self)
115
return RemoteTransport(self.base, self)
108
return SmartTransport(self.abspath(relative_url), self)
117
return RemoteTransport(self.abspath(relative_url), self)
110
119
def is_readonly(self):
111
120
"""Smart server transport can do read/write file operations."""
121
resp = self._call2('Transport.is_readonly')
122
if resp == ('yes', ):
124
elif resp == ('no', ):
127
self._translate_error(resp)
128
assert False, 'weird response %r' % (resp,)
114
130
def get_smart_client(self):
115
131
return self._medium
145
161
def _call2(self, method, *args):
146
162
"""Call a method on the remote server."""
147
protocol = SmartClientRequestProtocolOne(self._medium.get_request())
148
protocol.call(method, *args)
149
return protocol.read_response_tuple()
163
return client._SmartClient(self._medium).call(method, *args)
151
165
def _call_with_body_bytes(self, method, args, body):
152
166
"""Call a method on the remote server with body bytes."""
153
protocol = SmartClientRequestProtocolOne(self._medium.get_request())
154
protocol.call_with_body_bytes((method, ) + args, body)
155
return protocol.read_response_tuple()
167
smart_client = client._SmartClient(self._medium)
168
return smart_client.call_with_body_bytes(method, args, body)
157
170
def has(self, relpath):
158
171
"""Indicate whether a remote file of the given name exists or not.
177
190
def get_bytes(self, relpath):
178
191
remote = self._remote_path(relpath)
179
protocol = SmartClientRequestProtocolOne(self._medium.get_request())
180
protocol.call('get', remote)
181
resp = protocol.read_response_tuple(True)
192
request = self._medium.get_request()
193
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
194
smart_protocol.call('get', remote)
195
resp = smart_protocol.read_response_tuple(True)
182
196
if resp != ('ok', ):
183
protocol.cancel_read_body()
197
smart_protocol.cancel_read_body()
184
198
self._translate_error(resp, relpath)
185
return protocol.read_body_bytes()
199
return smart_protocol.read_body_bytes()
187
201
def _serialise_optional_mode(self, mode):
199
213
# FIXME: upload_file is probably not safe for non-ascii characters -
200
214
# should probably just pass all parameters as length-delimited
216
if type(upload_contents) is unicode:
217
# Although not strictly correct, we raise UnicodeEncodeError to be
218
# compatible with other transports.
219
raise UnicodeEncodeError(
220
'undefined', upload_contents, 0, 1,
221
'put_bytes must be given bytes, not unicode.')
202
222
resp = self._call_with_body_bytes('put',
203
223
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
268
288
limit=self._max_readv_combine,
269
289
fudge_factor=self._bytes_to_read_before_seek))
271
protocol = SmartClientRequestProtocolOne(self._medium.get_request())
272
protocol.call_with_body_readv_array(
291
request = self._medium.get_request()
292
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
293
smart_protocol.call_with_body_readv_array(
273
294
('readv', self._remote_path(relpath)),
274
295
[(c.start, c.length) for c in coalesced])
275
resp = protocol.read_response_tuple(True)
296
resp = smart_protocol.read_response_tuple(True)
277
298
if resp[0] != 'readv':
278
299
# This should raise an exception
279
protocol.cancel_read_body()
300
smart_protocol.cancel_read_body()
280
301
self._translate_error(resp)
283
304
# FIXME: this should know how many bytes are needed, for clarity.
284
data = protocol.read_body_bytes()
305
data = smart_protocol.read_body_bytes()
285
306
# Cache the results, but only until they have been fulfilled
287
308
for c_offset in coalesced:
363
384
def stat(self, relpath):
364
385
resp = self._call2('stat', self._remote_path(relpath))
365
386
if resp[0] == 'stat':
366
return SmartStat(int(resp[1]), int(resp[2], 8))
387
return _SmartStat(int(resp[1]), int(resp[2], 8))
368
389
self._translate_error(resp)
398
419
self._translate_error(resp)
402
class SmartTCPTransport(SmartTransport):
422
class RemoteTCPTransport(RemoteTransport):
403
423
"""Connection to smart server over plain tcp.
405
425
This is essentially just a factory to get 'RemoteTransport(url,
417
437
except (ValueError, TypeError), e:
418
438
raise errors.InvalidURL(
419
439
path=url, extra="invalid port %s" % _port)
420
medium = SmartTCPClientMedium(_host, _port)
421
super(SmartTCPTransport, self).__init__(url, medium=medium)
424
class SmartSSHTransport(SmartTransport):
440
client_medium = medium.SmartTCPClientMedium(_host, _port)
441
super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
444
class RemoteSSHTransport(RemoteTransport):
425
445
"""Connection to smart server over SSH.
427
447
This is essentially just a factory to get 'RemoteTransport(url,
437
457
except (ValueError, TypeError), e:
438
458
raise errors.InvalidURL(path=url, extra="invalid port %s" %
440
medium = SmartSSHClientMedium(_host, _port, _username, _password)
441
super(SmartSSHTransport, self).__init__(url, medium=medium)
444
class SmartHTTPTransport(SmartTransport):
460
client_medium = medium.SmartSSHClientMedium(_host, _port,
461
_username, _password)
462
super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
465
class RemoteHTTPTransport(RemoteTransport):
445
466
"""Just a way to connect between a bzr+http:// url and http://.
447
This connection operates slightly differently than the SmartSSHTransport.
468
This connection operates slightly differently than the RemoteSSHTransport.
448
469
It uses a plain http:// transport underneath, which defines what remote
449
470
.bzr/smart URL we are connected to. From there, all paths that are sent are
450
471
sent as relative paths, this way, the remote side can properly
462
483
self._http_transport = http_transport
463
484
http_medium = self._http_transport.get_smart_medium()
464
super(SmartHTTPTransport, self).__init__(url, medium=http_medium)
485
super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
466
487
def _remote_path(self, relpath):
467
488
"""After connecting HTTP Transport only deals in relative URLs."""
489
# Adjust the relpath based on which URL this smart transport is
491
base = self._http_transport.base
492
url = urlutils.join(self.base[len('bzr+'):], relpath)
493
url = urlutils.normalize_url(url)
494
return urlutils.relative_url(base, url)
473
496
def abspath(self, relpath):
474
497
"""Return the full url to the given relative path.
479
502
return self._unparse_url(self._combine_paths(self._path, relpath))
481
504
def clone(self, relative_url):
482
"""Make a new SmartHTTPTransport related to me.
505
"""Make a new RemoteHTTPTransport related to me.
484
507
This is re-implemented rather than using the default
485
SmartTransport.clone() because we must be careful about the underlying
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 "..").
489
520
abs_url = self.abspath(relative_url)
491
522
abs_url = self.base
492
# By cloning the underlying http_transport, we are able to share the
494
new_transport = self._http_transport.clone(relative_url)
495
return SmartHTTPTransport(abs_url, http_transport=new_transport)
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
normalized_rel_url = urlutils.relative_url(self.base, abs_url)
527
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
528
http_transport = self._http_transport.clone(normalized_rel_url)
530
http_transport = self._http_transport
531
return RemoteHTTPTransport(abs_url, http_transport=http_transport)
498
534
def get_test_permutations():
499
535
"""Return (transport, server) permutations for testing."""
500
from bzrlib.smart import server
501
536
### We may need a little more test framework support to construct an
502
537
### appropriate RemoteTransport in the future.
503
return [(SmartTCPTransport, server.SmartTCPServer_for_testing)]
538
from bzrlib.smart import server
539
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]