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,
96
_from_transport=_from_transport)
98
# The medium is the connection, except when we need to share it with
99
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
# what we want to share is really the shared connection.
102
if _from_transport is None:
103
# If no _from_transport is specified, we need to intialize the
107
medium, credentials = self._build_medium()
108
self._shared_connection= transport._SharedConnection(medium,
112
self._client = client._SmartClient(self.get_shared_medium())
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 put_bytes(self, relpath, upload_contents, mode=None):
212
# FIXME: upload_file is probably not safe for non-ascii characters -
213
# should probably just pass all parameters as length-delimited
215
if type(upload_contents) is unicode:
216
# Although not strictly correct, we raise UnicodeEncodeError to be
217
# compatible with other transports.
218
raise UnicodeEncodeError(
219
'undefined', upload_contents, 0, 1,
220
'put_bytes must be given bytes, not unicode.')
221
resp = self._call_with_body_bytes('put',
222
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
224
self._translate_error(resp)
226
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
227
create_parent_dir=False,
229
"""See Transport.put_bytes_non_atomic."""
230
# FIXME: no encoding in the transport!
231
create_parent_str = 'F'
232
if create_parent_dir:
233
create_parent_str = 'T'
235
resp = self._call_with_body_bytes(
237
(self._remote_path(relpath), self._serialise_optional_mode(mode),
238
create_parent_str, self._serialise_optional_mode(dir_mode)),
240
self._translate_error(resp)
242
def put_file(self, relpath, upload_file, mode=None):
243
# its not ideal to seek back, but currently put_non_atomic_file depends
244
# on transports not reading before failing - which is a faulty
245
# assumption I think - RBC 20060915
246
pos = upload_file.tell()
248
return self.put_bytes(relpath, upload_file.read(), mode)
250
upload_file.seek(pos)
253
def put_file_non_atomic(self, relpath, f, mode=None,
254
create_parent_dir=False,
256
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
257
create_parent_dir=create_parent_dir,
260
def append_file(self, relpath, from_file, mode=None):
261
return self.append_bytes(relpath, from_file.read(), mode)
263
def append_bytes(self, relpath, bytes, mode=None):
264
resp = self._call_with_body_bytes(
266
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
268
if resp[0] == 'appended':
270
self._translate_error(resp)
272
def delete(self, relpath):
273
resp = self._call2('delete', self._remote_path(relpath))
274
self._translate_error(resp)
276
def external_url(self):
277
"""See bzrlib.transport.Transport.external_url."""
278
# the external path for RemoteTransports is the base
281
def readv(self, relpath, offsets):
285
offsets = list(offsets)
287
sorted_offsets = sorted(offsets)
288
# turn the list of offsets into a stack
289
offset_stack = iter(offsets)
290
cur_offset_and_size = offset_stack.next()
291
coalesced = list(self._coalesce_offsets(sorted_offsets,
292
limit=self._max_readv_combine,
293
fudge_factor=self._bytes_to_read_before_seek))
295
request = self.get_smart_medium().get_request()
296
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
297
smart_protocol.call_with_body_readv_array(
298
('readv', self._remote_path(relpath)),
299
[(c.start, c.length) for c in coalesced])
300
resp = smart_protocol.read_response_tuple(True)
302
if resp[0] != 'readv':
303
# This should raise an exception
304
smart_protocol.cancel_read_body()
305
self._translate_error(resp)
308
# FIXME: this should know how many bytes are needed, for clarity.
309
data = smart_protocol.read_body_bytes()
310
# Cache the results, but only until they have been fulfilled
312
for c_offset in coalesced:
313
if len(data) < c_offset.length:
314
raise errors.ShortReadvError(relpath, c_offset.start,
315
c_offset.length, actual=len(data))
316
for suboffset, subsize in c_offset.ranges:
317
key = (c_offset.start+suboffset, subsize)
318
data_map[key] = data[suboffset:suboffset+subsize]
319
data = data[c_offset.length:]
321
# Now that we've read some data, see if we can yield anything back
322
while cur_offset_and_size in data_map:
323
this_data = data_map.pop(cur_offset_and_size)
324
yield cur_offset_and_size[0], this_data
325
cur_offset_and_size = offset_stack.next()
327
def rename(self, rel_from, rel_to):
329
self._remote_path(rel_from),
330
self._remote_path(rel_to))
332
def move(self, rel_from, rel_to):
334
self._remote_path(rel_from),
335
self._remote_path(rel_to))
337
def rmdir(self, relpath):
338
resp = self._call('rmdir', self._remote_path(relpath))
340
def _translate_error(self, resp, orig_path=None):
341
"""Raise an exception from a response"""
348
elif what == 'NoSuchFile':
349
if orig_path is not None:
350
error_path = orig_path
353
raise errors.NoSuchFile(error_path)
354
elif what == 'error':
355
raise errors.SmartProtocolError(unicode(resp[1]))
356
elif what == 'FileExists':
357
raise errors.FileExists(resp[1])
358
elif what == 'DirectoryNotEmpty':
359
raise errors.DirectoryNotEmpty(resp[1])
360
elif what == 'ShortReadvError':
361
raise errors.ShortReadvError(resp[1], int(resp[2]),
362
int(resp[3]), int(resp[4]))
363
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
364
encoding = str(resp[1]) # encoding must always be a string
368
reason = str(resp[5]) # reason must always be a string
369
if val.startswith('u:'):
370
val = val[2:].decode('utf-8')
371
elif val.startswith('s:'):
372
val = val[2:].decode('base64')
373
if what == 'UnicodeDecodeError':
374
raise UnicodeDecodeError(encoding, val, start, end, reason)
375
elif what == 'UnicodeEncodeError':
376
raise UnicodeEncodeError(encoding, val, start, end, reason)
377
elif what == "ReadOnlyError":
378
raise errors.TransportNotPossible('readonly transport')
379
elif what == "ReadError":
380
if orig_path is not None:
381
error_path = orig_path
384
raise errors.ReadError(error_path)
386
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
388
def disconnect(self):
389
self.get_smart_medium().disconnect()
391
def delete_tree(self, relpath):
392
raise errors.TransportNotPossible('readonly transport')
394
def stat(self, relpath):
395
resp = self._call2('stat', self._remote_path(relpath))
396
if resp[0] == 'stat':
397
return _SmartStat(int(resp[1]), int(resp[2], 8))
399
self._translate_error(resp)
401
## def lock_read(self, relpath):
402
## """Lock the given file for shared (read) access.
403
## :return: A lock object, which should be passed to Transport.unlock()
405
## # The old RemoteBranch ignore lock for reading, so we will
406
## # continue that tradition and return a bogus lock object.
407
## class BogusLock(object):
408
## def __init__(self, path):
412
## return BogusLock(relpath)
417
def list_dir(self, relpath):
418
resp = self._call2('list_dir', self._remote_path(relpath))
419
if resp[0] == 'names':
420
return [name.encode('ascii') for name in resp[1:]]
422
self._translate_error(resp)
424
def iter_files_recursive(self):
425
resp = self._call2('iter_files_recursive', self._remote_path(''))
426
if resp[0] == 'names':
429
self._translate_error(resp)
432
class RemoteTCPTransport(RemoteTransport):
433
"""Connection to smart server over plain tcp.
435
This is essentially just a factory to get 'RemoteTransport(url,
436
SmartTCPClientMedium).
439
def _build_medium(self):
440
assert self.base.startswith('bzr://')
441
if self._port is None:
442
self._port = BZR_DEFAULT_PORT
443
return medium.SmartTCPClientMedium(self._host, self._port), None
446
class RemoteSSHTransport(RemoteTransport):
447
"""Connection to smart server over SSH.
449
This is essentially just a factory to get 'RemoteTransport(url,
450
SmartSSHClientMedium).
453
def _build_medium(self):
454
assert self.base.startswith('bzr+ssh://')
455
# ssh will prompt the user for a password if needed and if none is
456
# provided but it will not give it back, so no credentials can be
458
return medium.SmartSSHClientMedium(self._host, self._port,
459
self._user, self._password), None
462
class RemoteHTTPTransport(RemoteTransport):
463
"""Just a way to connect between a bzr+http:// url and http://.
465
This connection operates slightly differently than the RemoteSSHTransport.
466
It uses a plain http:// transport underneath, which defines what remote
467
.bzr/smart URL we are connected to. From there, all paths that are sent are
468
sent as relative paths, this way, the remote side can properly
469
de-reference them, since it is likely doing rewrite rules to translate an
470
HTTP path into a local path.
473
def __init__(self, base, _from_transport=None, http_transport=None):
474
assert base.startswith('bzr+http://')
476
if http_transport is None:
477
# FIXME: the password may be lost here because it appears in the
478
# url only for an intial construction (when the url came from the
480
http_url = base[len('bzr+'):]
481
self._http_transport = transport.get_transport(http_url)
483
self._http_transport = http_transport
484
super(RemoteHTTPTransport, self).__init__(
485
base, _from_transport=_from_transport)
487
def _build_medium(self):
488
# We let http_transport take care of the credentials
489
return self._http_transport.get_smart_medium(), None
491
def _remote_path(self, relpath):
492
"""After connecting, HTTP Transport only deals in relative URLs."""
493
# Adjust the relpath based on which URL this smart transport is
495
http_base = urlutils.normalize_url(self._http_transport.base)
496
url = urlutils.join(self.base[len('bzr+'):], relpath)
497
url = urlutils.normalize_url(url)
498
return urlutils.relative_url(http_base, url)
500
def clone(self, relative_url):
501
"""Make a new RemoteHTTPTransport related to me.
503
This is re-implemented rather than using the default
504
RemoteTransport.clone() because we must be careful about the underlying
507
Also, the cloned smart transport will POST to the same .bzr/smart
508
location as this transport (although obviously the relative paths in the
509
smart requests may be different). This is so that the server doesn't
510
have to handle .bzr/smart requests at arbitrary places inside .bzr
511
directories, just at the initial URL the user uses.
513
The exception is parent paths (i.e. relative_url of "..").
516
abs_url = self.abspath(relative_url)
519
# We either use the exact same http_transport (for child locations), or
520
# a clone of the underlying http_transport (for parent locations). This
521
# means we share the connection.
522
norm_base = urlutils.normalize_url(self.base)
523
norm_abs_url = urlutils.normalize_url(abs_url)
524
normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
525
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
526
http_transport = self._http_transport.clone(normalized_rel_url)
528
http_transport = self._http_transport
529
return RemoteHTTPTransport(abs_url,
530
_from_transport=self,
531
http_transport=http_transport)
534
def get_test_permutations():
535
"""Return (transport, server) permutations for testing."""
536
### We may need a little more test framework support to construct an
537
### appropriate RemoteTransport in the future.
538
from bzrlib.smart import server
539
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]