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
34
from bzrlib.smart import client, medium, protocol
36
# must do this otherwise urllib can't parse the urls properly :(
37
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
38
transport.register_urlparse_netloc_protocol(scheme)
42
# Port 4155 is the default port for bzr://, registered with IANA.
43
BZR_DEFAULT_INTERFACE = '0.0.0.0'
44
BZR_DEFAULT_PORT = 4155
47
class _SmartStat(object):
49
def __init__(self, size, mode):
54
class RemoteTransport(transport.ConnectedTransport):
55
"""Connection to a smart server.
57
The connection holds references to the medium that can be used to send
58
requests to the server.
60
The connection has a notion of the current directory to which it's
61
connected; this is incorporated in filenames passed to the server.
63
This supports some higher-level RPC operations and can also be treated
64
like a Transport to do file-like operations.
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.
71
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
72
# responsibilities: Put those on SmartClient or similar. This is vital for
73
# the ability to support multiple versions of the smart protocol over time:
74
# RemoteTransport is an adapter from the Transport object model to the
75
# SmartClient model, not an encoder.
77
# FIXME: the medium parameter should be private, only the tests requires
78
# it. It may be even clearer to define a TestRemoteTransport that handles
79
# the specific cases of providing a _client and/or a _medium, and leave
80
# RemoteTransport as an abstract class.
81
def __init__(self, url, from_transport=None, medium=None, _client=None):
84
:param from_transport: Another RemoteTransport instance that this
85
one is being cloned from. Attributes such as the medium will
88
:param medium: The medium to use for this RemoteTransport. This must be
89
supplied if from_transport is None.
91
:param _client: Override the _SmartClient used by this transport. This
92
should only be used for testing purposes; normally this is
93
determined from the medium.
95
super(RemoteTransport, self).__init__(url, from_transport)
98
medium = self._build_medium(from_transport)
100
self._set_connection(medium, None)
101
self._medium = medium
102
assert self._medium is not None
103
# ConnectedTransport provides the connection sharing by requiring
104
# daughter classes to always access the connection via _get_connection
105
# and guarantee that the connection wil be shared across all transports
106
# even if the server force a reconnection (i.e. the connection object
107
# should be built again).
109
# For RemoteTransport objects, the medium is the connection. But using
110
# _get_connection is not enough because:
111
# - self._client needs a medium to be built (and keep an internal
113
# - RemoteBzrDir RemoteRepository, RemoteRepositoryFormat RemoteBranch
114
# keep internal copies of _SmartClient objects.
116
# Therefore, the needed refactoring (enhancing _SmartClient and its
117
# users so that connection are still shared even in cases or
118
# reconnections) is too much for this round -- vila20070607
120
# On the other hand, except for the reconnection part, the sharing will
121
# already reduce the number of connections.
123
self._client = client._SmartClient(self._medium)
125
self._client = _client
127
def _build_medium(self, from_transport=None):
128
"""Create the medium if from_transport does not provide one.
130
The medium is analogous to the connection for ConnectedTransport: it
131
allows connection sharing.
133
:param from_transport: provide the medium to reuse if not None
135
if from_transport is not None:
136
_medium = from_transport._medium
140
self._set_connection(_medium, None)
143
def is_readonly(self):
144
"""Smart server transport can do read/write file operations."""
145
resp = self._call2('Transport.is_readonly')
146
if resp == ('yes', ):
148
elif resp == ('no', ):
150
elif (resp == ('error', "Generic bzr smart protocol error: "
151
"bad request 'Transport.is_readonly'") or
152
resp == ('error', "Generic bzr smart protocol error: "
153
"bad request u'Transport.is_readonly'")):
154
# XXX: nasty hack: servers before 0.16 don't have a
155
# 'Transport.is_readonly' verb, so we do what clients before 0.16
159
self._translate_error(resp)
160
assert False, 'weird response %r' % (resp,)
162
def get_smart_client(self):
163
return self._get_connection()
165
def get_smart_medium(self):
166
return self._get_connection()
168
def _remote_path(self, relpath):
169
"""Returns the Unicode version of the absolute path for relpath."""
170
return self._combine_paths(self._path, relpath)
172
def _call(self, method, *args):
173
resp = self._call2(method, *args)
174
self._translate_error(resp)
176
def _call2(self, method, *args):
177
"""Call a method on the remote server."""
178
return self._client.call(method, *args)
180
def _call_with_body_bytes(self, method, args, body):
181
"""Call a method on the remote server with body bytes."""
182
return self._client.call_with_body_bytes(method, args, body)
184
def has(self, relpath):
185
"""Indicate whether a remote file of the given name exists or not.
187
:see: Transport.has()
189
resp = self._call2('has', self._remote_path(relpath))
190
if resp == ('yes', ):
192
elif resp == ('no', ):
195
self._translate_error(resp)
197
def get(self, relpath):
198
"""Return file-like object reading the contents of a remote file.
200
:see: Transport.get_bytes()/get_file()
202
return StringIO(self.get_bytes(relpath))
204
def get_bytes(self, relpath):
205
remote = self._remote_path(relpath)
206
request = self._medium.get_request()
207
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
208
smart_protocol.call('get', remote)
209
resp = smart_protocol.read_response_tuple(True)
211
smart_protocol.cancel_read_body()
212
self._translate_error(resp, relpath)
213
return smart_protocol.read_body_bytes()
215
def _serialise_optional_mode(self, mode):
221
def mkdir(self, relpath, mode=None):
222
resp = self._call2('mkdir', self._remote_path(relpath),
223
self._serialise_optional_mode(mode))
224
self._translate_error(resp)
226
def put_bytes(self, relpath, upload_contents, mode=None):
227
# FIXME: upload_file is probably not safe for non-ascii characters -
228
# should probably just pass all parameters as length-delimited
230
if type(upload_contents) is unicode:
231
# Although not strictly correct, we raise UnicodeEncodeError to be
232
# compatible with other transports.
233
raise UnicodeEncodeError(
234
'undefined', upload_contents, 0, 1,
235
'put_bytes must be given bytes, not unicode.')
236
resp = self._call_with_body_bytes('put',
237
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
239
self._translate_error(resp)
241
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
242
create_parent_dir=False,
244
"""See Transport.put_bytes_non_atomic."""
245
# FIXME: no encoding in the transport!
246
create_parent_str = 'F'
247
if create_parent_dir:
248
create_parent_str = 'T'
250
resp = self._call_with_body_bytes(
252
(self._remote_path(relpath), self._serialise_optional_mode(mode),
253
create_parent_str, self._serialise_optional_mode(dir_mode)),
255
self._translate_error(resp)
257
def put_file(self, relpath, upload_file, mode=None):
258
# its not ideal to seek back, but currently put_non_atomic_file depends
259
# on transports not reading before failing - which is a faulty
260
# assumption I think - RBC 20060915
261
pos = upload_file.tell()
263
return self.put_bytes(relpath, upload_file.read(), mode)
265
upload_file.seek(pos)
268
def put_file_non_atomic(self, relpath, f, mode=None,
269
create_parent_dir=False,
271
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
272
create_parent_dir=create_parent_dir,
275
def append_file(self, relpath, from_file, mode=None):
276
return self.append_bytes(relpath, from_file.read(), mode)
278
def append_bytes(self, relpath, bytes, mode=None):
279
resp = self._call_with_body_bytes(
281
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
283
if resp[0] == 'appended':
285
self._translate_error(resp)
287
def delete(self, relpath):
288
resp = self._call2('delete', self._remote_path(relpath))
289
self._translate_error(resp)
291
def readv(self, relpath, offsets):
295
offsets = list(offsets)
297
sorted_offsets = sorted(offsets)
298
# turn the list of offsets into a stack
299
offset_stack = iter(offsets)
300
cur_offset_and_size = offset_stack.next()
301
coalesced = list(self._coalesce_offsets(sorted_offsets,
302
limit=self._max_readv_combine,
303
fudge_factor=self._bytes_to_read_before_seek))
305
request = self._medium.get_request()
306
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
307
smart_protocol.call_with_body_readv_array(
308
('readv', self._remote_path(relpath)),
309
[(c.start, c.length) for c in coalesced])
310
resp = smart_protocol.read_response_tuple(True)
312
if resp[0] != 'readv':
313
# This should raise an exception
314
smart_protocol.cancel_read_body()
315
self._translate_error(resp)
318
# FIXME: this should know how many bytes are needed, for clarity.
319
data = smart_protocol.read_body_bytes()
320
# Cache the results, but only until they have been fulfilled
322
for c_offset in coalesced:
323
if len(data) < c_offset.length:
324
raise errors.ShortReadvError(relpath, c_offset.start,
325
c_offset.length, actual=len(data))
326
for suboffset, subsize in c_offset.ranges:
327
key = (c_offset.start+suboffset, subsize)
328
data_map[key] = data[suboffset:suboffset+subsize]
329
data = data[c_offset.length:]
331
# Now that we've read some data, see if we can yield anything back
332
while cur_offset_and_size in data_map:
333
this_data = data_map.pop(cur_offset_and_size)
334
yield cur_offset_and_size[0], this_data
335
cur_offset_and_size = offset_stack.next()
337
def rename(self, rel_from, rel_to):
339
self._remote_path(rel_from),
340
self._remote_path(rel_to))
342
def move(self, rel_from, rel_to):
344
self._remote_path(rel_from),
345
self._remote_path(rel_to))
347
def rmdir(self, relpath):
348
resp = self._call('rmdir', self._remote_path(relpath))
350
def _translate_error(self, resp, orig_path=None):
351
"""Raise an exception from a response"""
358
elif what == 'NoSuchFile':
359
if orig_path is not None:
360
error_path = orig_path
363
raise errors.NoSuchFile(error_path)
364
elif what == 'error':
365
raise errors.SmartProtocolError(unicode(resp[1]))
366
elif what == 'FileExists':
367
raise errors.FileExists(resp[1])
368
elif what == 'DirectoryNotEmpty':
369
raise errors.DirectoryNotEmpty(resp[1])
370
elif what == 'ShortReadvError':
371
raise errors.ShortReadvError(resp[1], int(resp[2]),
372
int(resp[3]), int(resp[4]))
373
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
374
encoding = str(resp[1]) # encoding must always be a string
378
reason = str(resp[5]) # reason must always be a string
379
if val.startswith('u:'):
380
val = val[2:].decode('utf-8')
381
elif val.startswith('s:'):
382
val = val[2:].decode('base64')
383
if what == 'UnicodeDecodeError':
384
raise UnicodeDecodeError(encoding, val, start, end, reason)
385
elif what == 'UnicodeEncodeError':
386
raise UnicodeEncodeError(encoding, val, start, end, reason)
387
elif what == "ReadOnlyError":
388
raise errors.TransportNotPossible('readonly transport')
390
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
392
def disconnect(self):
393
self._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 __init__(self, base, from_transport=None):
444
assert base.startswith('bzr://')
445
super(RemoteTCPTransport, self).__init__(base, from_transport)
446
if self._port is None:
447
self._port = BZR_DEFAULT_PORT
449
def _build_medium(self, from_transport=None):
450
if from_transport is not None:
451
_medium = from_transport._medium
453
_medium = medium.SmartTCPClientMedium(self._host, self._port)
454
self._set_connection(_medium, None)
458
class RemoteSSHTransport(RemoteTransport):
459
"""Connection to smart server over SSH.
461
This is essentially just a factory to get 'RemoteTransport(url,
462
SmartSSHClientMedium).
465
def _build_medium(self, from_transport=None):
466
assert self.base.startswith('bzr+ssh://')
467
if from_transport is not None:
468
_medium = from_transport._medium
470
_medium = medium.SmartSSHClientMedium(self._host, self._port,
471
self._user, self._password)
472
# ssh will prompt the user for a password if needed and if none is
473
# provided but it will not give it back, so no credentials can be
475
self._set_connection(_medium, None)
479
class RemoteHTTPTransport(RemoteTransport):
480
"""Just a way to connect between a bzr+http:// url and http://.
482
This connection operates slightly differently than the RemoteSSHTransport.
483
It uses a plain http:// transport underneath, which defines what remote
484
.bzr/smart URL we are connected to. From there, all paths that are sent are
485
sent as relative paths, this way, the remote side can properly
486
de-reference them, since it is likely doing rewrite rules to translate an
487
HTTP path into a local path.
490
def __init__(self, base, from_transport=None, http_transport=None):
491
assert base.startswith('bzr+http://')
493
if http_transport is None:
494
# FIXME: the password may be lost here because it appears in the
495
# url only for an intial construction (when the url came from the
497
http_url = base[len('bzr+'):]
498
self._http_transport = transport.get_transport(http_url)
500
self._http_transport = http_transport
501
super(RemoteHTTPTransport, self).__init__(base, from_transport)
503
def _build_medium(self, from_transport=None):
504
if from_transport is not None:
505
_medium = from_transport._medium
507
_medium = self._http_transport.get_smart_medium()
508
# We let http_transport take care of the credentials
509
self._set_connection(_medium, None)
512
def _remote_path(self, relpath):
513
"""After connecting, HTTP Transport only deals in relative URLs."""
514
# Adjust the relpath based on which URL this smart transport is
516
http_base = urlutils.normalize_url(self._http_transport.base)
517
url = urlutils.join(self.base[len('bzr+'):], relpath)
518
url = urlutils.normalize_url(url)
519
return urlutils.relative_url(http_base, url)
521
def clone(self, relative_url):
522
"""Make a new RemoteHTTPTransport related to me.
524
This is re-implemented rather than using the default
525
RemoteTransport.clone() because we must be careful about the underlying
528
Also, the cloned smart transport will POST to the same .bzr/smart
529
location as this transport (although obviously the relative paths in the
530
smart requests may be different). This is so that the server doesn't
531
have to handle .bzr/smart requests at arbitrary places inside .bzr
532
directories, just at the initial URL the user uses.
534
The exception is parent paths (i.e. relative_url of "..").
537
abs_url = self.abspath(relative_url)
540
# We either use the exact same http_transport (for child locations), or
541
# a clone of the underlying http_transport (for parent locations). This
542
# means we share the connection.
543
norm_base = urlutils.normalize_url(self.base)
544
norm_abs_url = urlutils.normalize_url(abs_url)
545
normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
546
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
547
http_transport = self._http_transport.clone(normalized_rel_url)
549
http_transport = self._http_transport
550
return RemoteHTTPTransport(abs_url, self, http_transport=http_transport)
553
def get_test_permutations():
554
"""Return (transport, server) permutations for testing."""
555
### We may need a little more test framework support to construct an
556
### appropriate RemoteTransport in the future.
557
from bzrlib.smart import server
558
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]