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
33
from bzrlib.smart import client, medium, protocol
35
# must do this otherwise urllib can't parse the urls properly :(
36
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
37
transport.register_urlparse_netloc_protocol(scheme)
41
# Port 4155 is the default port for bzr://, registered with IANA.
42
BZR_DEFAULT_INTERFACE = '0.0.0.0'
43
BZR_DEFAULT_PORT = 4155
46
class _SmartStat(object):
48
def __init__(self, size, mode):
53
class RemoteTransport(transport.Transport):
54
"""Connection to a smart server.
56
The connection holds references to the medium that can be used to send
57
requests to the server.
59
The connection has a notion of the current directory to which it's
60
connected; this is incorporated in filenames passed to the server.
62
This supports some higher-level RPC operations and can also be treated
63
like a Transport to do file-like operations.
65
The connection can be made over a tcp socket, an ssh pipe or a series of
66
http requests. There are concrete subclasses for each type:
67
RemoteTCPTransport, etc.
70
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
71
# responsibilities: Put those on SmartClient or similar. This is vital for
72
# the ability to support multiple versions of the smart protocol over time:
73
# RemoteTransport is an adapter from the Transport object model to the
74
# SmartClient model, not an encoder.
76
def __init__(self, url, clone_from=None, medium=None):
79
:param medium: The medium to use for this RemoteTransport. This must be
80
supplied if clone_from is None.
82
### Technically super() here is faulty because Transport's __init__
83
### fails to take 2 parameters, and if super were to choose a silly
84
### initialisation order things would blow up.
85
if not url.endswith('/'):
87
super(RemoteTransport, self).__init__(url)
88
self._scheme, self._username, self._password, self._host, self._port, self._path = \
89
transport.split_url(url)
90
if clone_from is None:
93
# credentials may be stripped from the base in some circumstances
94
# as yet to be clearly defined or documented, so copy them.
95
self._username = clone_from._username
96
# reuse same connection
97
self._medium = clone_from._medium
98
assert self._medium is not None
100
def abspath(self, relpath):
101
"""Return the full url to the given relative path.
103
@param relpath: the relative path or path components
104
@type relpath: str or list
106
return self._unparse_url(self._remote_path(relpath))
108
def clone(self, relative_url):
109
"""Make a new RemoteTransport related to me, sharing the same connection.
111
This essentially opens a handle on a different remote directory.
113
if relative_url is None:
114
return RemoteTransport(self.base, self)
116
return RemoteTransport(self.abspath(relative_url), self)
118
def is_readonly(self):
119
"""Smart server transport can do read/write file operations."""
120
resp = self._call2('Transport.is_readonly')
121
if resp == ('yes', ):
123
elif resp == ('no', ):
126
self._translate_error(resp)
127
assert False, 'weird response %r' % (resp,)
129
def get_smart_client(self):
132
def get_smart_medium(self):
135
def _unparse_url(self, path):
136
"""Return URL for a path.
138
:see: SFTPUrlHandling._unparse_url
140
# TODO: Eventually it should be possible to unify this with
141
# SFTPUrlHandling._unparse_url?
144
path = urllib.quote(path)
145
netloc = urllib.quote(self._host)
146
if self._username is not None:
147
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
148
if self._port is not None:
149
netloc = '%s:%d' % (netloc, self._port)
150
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
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 client._SmartClient(self._medium).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
smart_client = client._SmartClient(self._medium)
167
return smart_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._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 readv(self, relpath, offsets):
280
offsets = list(offsets)
282
sorted_offsets = sorted(offsets)
283
# turn the list of offsets into a stack
284
offset_stack = iter(offsets)
285
cur_offset_and_size = offset_stack.next()
286
coalesced = list(self._coalesce_offsets(sorted_offsets,
287
limit=self._max_readv_combine,
288
fudge_factor=self._bytes_to_read_before_seek))
290
request = self._medium.get_request()
291
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
292
smart_protocol.call_with_body_readv_array(
293
('readv', self._remote_path(relpath)),
294
[(c.start, c.length) for c in coalesced])
295
resp = smart_protocol.read_response_tuple(True)
297
if resp[0] != 'readv':
298
# This should raise an exception
299
smart_protocol.cancel_read_body()
300
self._translate_error(resp)
303
# FIXME: this should know how many bytes are needed, for clarity.
304
data = smart_protocol.read_body_bytes()
305
# Cache the results, but only until they have been fulfilled
307
for c_offset in coalesced:
308
if len(data) < c_offset.length:
309
raise errors.ShortReadvError(relpath, c_offset.start,
310
c_offset.length, actual=len(data))
311
for suboffset, subsize in c_offset.ranges:
312
key = (c_offset.start+suboffset, subsize)
313
data_map[key] = data[suboffset:suboffset+subsize]
314
data = data[c_offset.length:]
316
# Now that we've read some data, see if we can yield anything back
317
while cur_offset_and_size in data_map:
318
this_data = data_map.pop(cur_offset_and_size)
319
yield cur_offset_and_size[0], this_data
320
cur_offset_and_size = offset_stack.next()
322
def rename(self, rel_from, rel_to):
324
self._remote_path(rel_from),
325
self._remote_path(rel_to))
327
def move(self, rel_from, rel_to):
329
self._remote_path(rel_from),
330
self._remote_path(rel_to))
332
def rmdir(self, relpath):
333
resp = self._call('rmdir', self._remote_path(relpath))
335
def _translate_error(self, resp, orig_path=None):
336
"""Raise an exception from a response"""
343
elif what == 'NoSuchFile':
344
if orig_path is not None:
345
error_path = orig_path
348
raise errors.NoSuchFile(error_path)
349
elif what == 'error':
350
raise errors.SmartProtocolError(unicode(resp[1]))
351
elif what == 'FileExists':
352
raise errors.FileExists(resp[1])
353
elif what == 'DirectoryNotEmpty':
354
raise errors.DirectoryNotEmpty(resp[1])
355
elif what == 'ShortReadvError':
356
raise errors.ShortReadvError(resp[1], int(resp[2]),
357
int(resp[3]), int(resp[4]))
358
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
359
encoding = str(resp[1]) # encoding must always be a string
363
reason = str(resp[5]) # reason must always be a string
364
if val.startswith('u:'):
365
val = val[2:].decode('utf-8')
366
elif val.startswith('s:'):
367
val = val[2:].decode('base64')
368
if what == 'UnicodeDecodeError':
369
raise UnicodeDecodeError(encoding, val, start, end, reason)
370
elif what == 'UnicodeEncodeError':
371
raise UnicodeEncodeError(encoding, val, start, end, reason)
372
elif what == "ReadOnlyError":
373
raise errors.TransportNotPossible('readonly transport')
375
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
377
def disconnect(self):
378
self._medium.disconnect()
380
def delete_tree(self, relpath):
381
raise errors.TransportNotPossible('readonly transport')
383
def stat(self, relpath):
384
resp = self._call2('stat', self._remote_path(relpath))
385
if resp[0] == 'stat':
386
return _SmartStat(int(resp[1]), int(resp[2], 8))
388
self._translate_error(resp)
390
## def lock_read(self, relpath):
391
## """Lock the given file for shared (read) access.
392
## :return: A lock object, which should be passed to Transport.unlock()
394
## # The old RemoteBranch ignore lock for reading, so we will
395
## # continue that tradition and return a bogus lock object.
396
## class BogusLock(object):
397
## def __init__(self, path):
401
## return BogusLock(relpath)
406
def list_dir(self, relpath):
407
resp = self._call2('list_dir', self._remote_path(relpath))
408
if resp[0] == 'names':
409
return [name.encode('ascii') for name in resp[1:]]
411
self._translate_error(resp)
413
def iter_files_recursive(self):
414
resp = self._call2('iter_files_recursive', self._remote_path(''))
415
if resp[0] == 'names':
418
self._translate_error(resp)
421
class RemoteTCPTransport(RemoteTransport):
422
"""Connection to smart server over plain tcp.
424
This is essentially just a factory to get 'RemoteTransport(url,
425
SmartTCPClientMedium).
428
def __init__(self, url):
429
_scheme, _username, _password, _host, _port, _path = \
430
transport.split_url(url)
432
_port = BZR_DEFAULT_PORT
436
except (ValueError, TypeError), e:
437
raise errors.InvalidURL(
438
path=url, extra="invalid port %s" % _port)
439
client_medium = medium.SmartTCPClientMedium(_host, _port)
440
super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
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 __init__(self, url):
451
_scheme, _username, _password, _host, _port, _path = \
452
transport.split_url(url)
454
if _port is not None:
456
except (ValueError, TypeError), e:
457
raise errors.InvalidURL(path=url, extra="invalid port %s" %
459
client_medium = medium.SmartSSHClientMedium(_host, _port,
460
_username, _password)
461
super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
464
class RemoteHTTPTransport(RemoteTransport):
465
"""Just a way to connect between a bzr+http:// url and http://.
467
This connection operates slightly differently than the RemoteSSHTransport.
468
It uses a plain http:// transport underneath, which defines what remote
469
.bzr/smart URL we are connected to. From there, all paths that are sent are
470
sent as relative paths, this way, the remote side can properly
471
de-reference them, since it is likely doing rewrite rules to translate an
472
HTTP path into a local path.
475
def __init__(self, url, http_transport=None):
476
assert url.startswith('bzr+http://')
478
if http_transport is None:
479
http_url = url[len('bzr+'):]
480
self._http_transport = transport.get_transport(http_url)
482
self._http_transport = http_transport
483
http_medium = self._http_transport.get_smart_medium()
484
super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
486
def _remote_path(self, relpath):
487
"""After connecting HTTP Transport only deals in relative URLs."""
493
def abspath(self, relpath):
494
"""Return the full url to the given relative path.
496
:param relpath: the relative path or path components
497
:type relpath: str or list
499
return self._unparse_url(self._combine_paths(self._path, relpath))
501
def clone(self, relative_url):
502
"""Make a new RemoteHTTPTransport related to me.
504
This is re-implemented rather than using the default
505
RemoteTransport.clone() because we must be careful about the underlying
509
abs_url = self.abspath(relative_url)
512
# By cloning the underlying http_transport, we are able to share the
514
new_transport = self._http_transport.clone(relative_url)
515
return RemoteHTTPTransport(abs_url, http_transport=new_transport)
518
def get_test_permutations():
519
"""Return (transport, server) permutations for testing."""
520
### We may need a little more test framework support to construct an
521
### appropriate RemoteTransport in the future.
522
from bzrlib.smart import server
523
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]