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)
97
# The medium is the connection, except when we need to share it with
98
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
99
# what we want to share is really the shared connection.
101
if from_transport is None:
102
# If no from_transport is specified, we need to intialize the
106
medium, credentials = self._build_medium()
107
self._shared_connection= transport._SharedConnection(medium,
111
self._client = client._SmartClient(self.get_shared_medium())
113
self._client = _client
115
def _build_medium(self):
116
"""Create the medium if from_transport does not provide one.
118
The medium is analogous to the connection for ConnectedTransport: it
119
allows connection sharing.
124
def is_readonly(self):
125
"""Smart server transport can do read/write file operations."""
126
resp = self._call2('Transport.is_readonly')
127
if resp == ('yes', ):
129
elif resp == ('no', ):
131
elif (resp == ('error', "Generic bzr smart protocol error: "
132
"bad request 'Transport.is_readonly'") or
133
resp == ('error', "Generic bzr smart protocol error: "
134
"bad request u'Transport.is_readonly'")):
135
# XXX: nasty hack: servers before 0.16 don't have a
136
# 'Transport.is_readonly' verb, so we do what clients before 0.16
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
def get_shared_medium(self):
150
return self._get_shared_connection()
152
def _remote_path(self, relpath):
153
"""Returns the Unicode version of the absolute path for relpath."""
154
return self._combine_paths(self._path, relpath)
156
def _call(self, method, *args):
157
resp = self._call2(method, *args)
158
self._translate_error(resp)
160
def _call2(self, method, *args):
161
"""Call a method on the remote server."""
162
return self._client.call(method, *args)
164
def _call_with_body_bytes(self, method, args, body):
165
"""Call a method on the remote server with body bytes."""
166
return self._client.call_with_body_bytes(method, args, body)
168
def has(self, relpath):
169
"""Indicate whether a remote file of the given name exists or not.
171
:see: Transport.has()
173
resp = self._call2('has', self._remote_path(relpath))
174
if resp == ('yes', ):
176
elif resp == ('no', ):
179
self._translate_error(resp)
181
def get(self, relpath):
182
"""Return file-like object reading the contents of a remote file.
184
:see: Transport.get_bytes()/get_file()
186
return StringIO(self.get_bytes(relpath))
188
def get_bytes(self, relpath):
189
remote = self._remote_path(relpath)
190
request = self.get_smart_medium().get_request()
191
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
192
smart_protocol.call('get', remote)
193
resp = smart_protocol.read_response_tuple(True)
195
smart_protocol.cancel_read_body()
196
self._translate_error(resp, relpath)
197
return smart_protocol.read_body_bytes()
199
def _serialise_optional_mode(self, mode):
205
def mkdir(self, relpath, mode=None):
206
resp = self._call2('mkdir', self._remote_path(relpath),
207
self._serialise_optional_mode(mode))
208
self._translate_error(resp)
210
def put_bytes(self, relpath, upload_contents, mode=None):
211
# FIXME: upload_file is probably not safe for non-ascii characters -
212
# should probably just pass all parameters as length-delimited
214
if type(upload_contents) is unicode:
215
# Although not strictly correct, we raise UnicodeEncodeError to be
216
# compatible with other transports.
217
raise UnicodeEncodeError(
218
'undefined', upload_contents, 0, 1,
219
'put_bytes must be given bytes, not unicode.')
220
resp = self._call_with_body_bytes('put',
221
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
223
self._translate_error(resp)
225
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
226
create_parent_dir=False,
228
"""See Transport.put_bytes_non_atomic."""
229
# FIXME: no encoding in the transport!
230
create_parent_str = 'F'
231
if create_parent_dir:
232
create_parent_str = 'T'
234
resp = self._call_with_body_bytes(
236
(self._remote_path(relpath), self._serialise_optional_mode(mode),
237
create_parent_str, self._serialise_optional_mode(dir_mode)),
239
self._translate_error(resp)
241
def put_file(self, relpath, upload_file, mode=None):
242
# its not ideal to seek back, but currently put_non_atomic_file depends
243
# on transports not reading before failing - which is a faulty
244
# assumption I think - RBC 20060915
245
pos = upload_file.tell()
247
return self.put_bytes(relpath, upload_file.read(), mode)
249
upload_file.seek(pos)
252
def put_file_non_atomic(self, relpath, f, mode=None,
253
create_parent_dir=False,
255
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
256
create_parent_dir=create_parent_dir,
259
def append_file(self, relpath, from_file, mode=None):
260
return self.append_bytes(relpath, from_file.read(), mode)
262
def append_bytes(self, relpath, bytes, mode=None):
263
resp = self._call_with_body_bytes(
265
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
267
if resp[0] == 'appended':
269
self._translate_error(resp)
271
def delete(self, relpath):
272
resp = self._call2('delete', self._remote_path(relpath))
273
self._translate_error(resp)
275
def readv(self, relpath, offsets):
279
offsets = list(offsets)
281
sorted_offsets = sorted(offsets)
282
# turn the list of offsets into a stack
283
offset_stack = iter(offsets)
284
cur_offset_and_size = offset_stack.next()
285
coalesced = list(self._coalesce_offsets(sorted_offsets,
286
limit=self._max_readv_combine,
287
fudge_factor=self._bytes_to_read_before_seek))
289
request = self.get_smart_medium().get_request()
290
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
291
smart_protocol.call_with_body_readv_array(
292
('readv', self._remote_path(relpath)),
293
[(c.start, c.length) for c in coalesced])
294
resp = smart_protocol.read_response_tuple(True)
296
if resp[0] != 'readv':
297
# This should raise an exception
298
smart_protocol.cancel_read_body()
299
self._translate_error(resp)
302
# FIXME: this should know how many bytes are needed, for clarity.
303
data = smart_protocol.read_body_bytes()
304
# Cache the results, but only until they have been fulfilled
306
for c_offset in coalesced:
307
if len(data) < c_offset.length:
308
raise errors.ShortReadvError(relpath, c_offset.start,
309
c_offset.length, actual=len(data))
310
for suboffset, subsize in c_offset.ranges:
311
key = (c_offset.start+suboffset, subsize)
312
data_map[key] = data[suboffset:suboffset+subsize]
313
data = data[c_offset.length:]
315
# Now that we've read some data, see if we can yield anything back
316
while cur_offset_and_size in data_map:
317
this_data = data_map.pop(cur_offset_and_size)
318
yield cur_offset_and_size[0], this_data
319
cur_offset_and_size = offset_stack.next()
321
def rename(self, rel_from, rel_to):
323
self._remote_path(rel_from),
324
self._remote_path(rel_to))
326
def move(self, rel_from, rel_to):
328
self._remote_path(rel_from),
329
self._remote_path(rel_to))
331
def rmdir(self, relpath):
332
resp = self._call('rmdir', self._remote_path(relpath))
334
def _translate_error(self, resp, orig_path=None):
335
"""Raise an exception from a response"""
342
elif what == 'NoSuchFile':
343
if orig_path is not None:
344
error_path = orig_path
347
raise errors.NoSuchFile(error_path)
348
elif what == 'error':
349
raise errors.SmartProtocolError(unicode(resp[1]))
350
elif what == 'FileExists':
351
raise errors.FileExists(resp[1])
352
elif what == 'DirectoryNotEmpty':
353
raise errors.DirectoryNotEmpty(resp[1])
354
elif what == 'ShortReadvError':
355
raise errors.ShortReadvError(resp[1], int(resp[2]),
356
int(resp[3]), int(resp[4]))
357
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
358
encoding = str(resp[1]) # encoding must always be a string
362
reason = str(resp[5]) # reason must always be a string
363
if val.startswith('u:'):
364
val = val[2:].decode('utf-8')
365
elif val.startswith('s:'):
366
val = val[2:].decode('base64')
367
if what == 'UnicodeDecodeError':
368
raise UnicodeDecodeError(encoding, val, start, end, reason)
369
elif what == 'UnicodeEncodeError':
370
raise UnicodeEncodeError(encoding, val, start, end, reason)
371
elif what == "ReadOnlyError":
372
raise errors.TransportNotPossible('readonly transport')
373
elif what == "ReadError":
374
if orig_path is not None:
375
error_path = orig_path
378
raise errors.ReadError(error_path)
380
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
382
def disconnect(self):
383
self.get_smart_medium().disconnect()
385
def delete_tree(self, relpath):
386
raise errors.TransportNotPossible('readonly transport')
388
def stat(self, relpath):
389
resp = self._call2('stat', self._remote_path(relpath))
390
if resp[0] == 'stat':
391
return _SmartStat(int(resp[1]), int(resp[2], 8))
393
self._translate_error(resp)
395
## def lock_read(self, relpath):
396
## """Lock the given file for shared (read) access.
397
## :return: A lock object, which should be passed to Transport.unlock()
399
## # The old RemoteBranch ignore lock for reading, so we will
400
## # continue that tradition and return a bogus lock object.
401
## class BogusLock(object):
402
## def __init__(self, path):
406
## return BogusLock(relpath)
411
def list_dir(self, relpath):
412
resp = self._call2('list_dir', self._remote_path(relpath))
413
if resp[0] == 'names':
414
return [name.encode('ascii') for name in resp[1:]]
416
self._translate_error(resp)
418
def iter_files_recursive(self):
419
resp = self._call2('iter_files_recursive', self._remote_path(''))
420
if resp[0] == 'names':
423
self._translate_error(resp)
426
class RemoteTCPTransport(RemoteTransport):
427
"""Connection to smart server over plain tcp.
429
This is essentially just a factory to get 'RemoteTransport(url,
430
SmartTCPClientMedium).
433
def __init__(self, base, from_transport=None):
434
assert base.startswith('bzr://')
435
super(RemoteTCPTransport, self).__init__(base, from_transport)
437
def _build_medium(self):
438
if self._port is None:
439
self._port = BZR_DEFAULT_PORT
440
return medium.SmartTCPClientMedium(self._host, self._port), None
443
class RemoteSSHTransport(RemoteTransport):
444
"""Connection to smart server over SSH.
446
This is essentially just a factory to get 'RemoteTransport(url,
447
SmartSSHClientMedium).
450
def _build_medium(self):
451
assert self.base.startswith('bzr+ssh://')
452
# ssh will prompt the user for a password if needed and if none is
453
# provided but it will not give it back, so no credentials can be
455
return medium.SmartSSHClientMedium(self._host, self._port,
456
self._user, self._password), None
459
class RemoteHTTPTransport(RemoteTransport):
460
"""Just a way to connect between a bzr+http:// url and http://.
462
This connection operates slightly differently than the RemoteSSHTransport.
463
It uses a plain http:// transport underneath, which defines what remote
464
.bzr/smart URL we are connected to. From there, all paths that are sent are
465
sent as relative paths, this way, the remote side can properly
466
de-reference them, since it is likely doing rewrite rules to translate an
467
HTTP path into a local path.
470
def __init__(self, base, from_transport=None, http_transport=None):
471
assert base.startswith('bzr+http://')
473
if http_transport is None:
474
# FIXME: the password may be lost here because it appears in the
475
# url only for an intial construction (when the url came from the
477
http_url = base[len('bzr+'):]
478
self._http_transport = transport.get_transport(http_url)
480
self._http_transport = http_transport
481
super(RemoteHTTPTransport, self).__init__(base, from_transport)
483
def _build_medium(self):
484
# We let http_transport take care of the credentials
485
return self._http_transport.get_smart_medium(), None
487
def _remote_path(self, relpath):
488
"""After connecting, HTTP Transport only deals in relative URLs."""
489
# Adjust the relpath based on which URL this smart transport is
491
http_base = urlutils.normalize_url(self._http_transport.base)
492
url = urlutils.join(self.base[len('bzr+'):], relpath)
493
url = urlutils.normalize_url(url)
494
return urlutils.relative_url(http_base, url)
496
def clone(self, relative_url):
497
"""Make a new RemoteHTTPTransport related to me.
499
This is re-implemented rather than using the default
500
RemoteTransport.clone() because we must be careful about the underlying
503
Also, the cloned smart transport will POST to the same .bzr/smart
504
location as this transport (although obviously the relative paths in the
505
smart requests may be different). This is so that the server doesn't
506
have to handle .bzr/smart requests at arbitrary places inside .bzr
507
directories, just at the initial URL the user uses.
509
The exception is parent paths (i.e. relative_url of "..").
512
abs_url = self.abspath(relative_url)
515
# We either use the exact same http_transport (for child locations), or
516
# a clone of the underlying http_transport (for parent locations). This
517
# means we share the connection.
518
norm_base = urlutils.normalize_url(self.base)
519
norm_abs_url = urlutils.normalize_url(abs_url)
520
normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
521
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
522
http_transport = self._http_transport.clone(normalized_rel_url)
524
http_transport = self._http_transport
525
return RemoteHTTPTransport(abs_url, self, http_transport=http_transport)
528
def get_test_permutations():
529
"""Return (transport, server) permutations for testing."""
530
### We may need a little more test framework support to construct an
531
### appropriate RemoteTransport in the future.
532
from bzrlib.smart import server
533
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]