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_PORT = 4155
45
class _SmartStat(object):
47
def __init__(self, size, mode):
52
class RemoteTransport(transport.Transport):
53
"""Connection to a smart server.
55
The connection holds references to the medium that can be used to send
56
requests to the server.
58
The connection has a notion of the current directory to which it's
59
connected; this is incorporated in filenames passed to the server.
61
This supports some higher-level RPC operations and can also be treated
62
like a Transport to do file-like operations.
64
The connection can be made over a tcp socket, an ssh pipe or a series of
65
http requests. There are concrete subclasses for each type:
66
RemoteTCPTransport, etc.
69
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
70
# responsibilities: Put those on SmartClient or similar. This is vital for
71
# the ability to support multiple versions of the smart protocol over time:
72
# RemoteTransport is an adapter from the Transport object model to the
73
# SmartClient model, not an encoder.
75
def __init__(self, url, clone_from=None, medium=None):
78
:param medium: The medium to use for this RemoteTransport. This must be
79
supplied if clone_from is None.
81
### Technically super() here is faulty because Transport's __init__
82
### fails to take 2 parameters, and if super were to choose a silly
83
### initialisation order things would blow up.
84
if not url.endswith('/'):
86
super(RemoteTransport, self).__init__(url)
87
self._scheme, self._username, self._password, self._host, self._port, self._path = \
88
transport.split_url(url)
89
if clone_from is None:
92
# credentials may be stripped from the base in some circumstances
93
# as yet to be clearly defined or documented, so copy them.
94
self._username = clone_from._username
95
# reuse same connection
96
self._medium = clone_from._medium
97
assert self._medium is not None
99
def abspath(self, relpath):
100
"""Return the full url to the given relative path.
102
@param relpath: the relative path or path components
103
@type relpath: str or list
105
return self._unparse_url(self._remote_path(relpath))
107
def clone(self, relative_url):
108
"""Make a new RemoteTransport related to me, sharing the same connection.
110
This essentially opens a handle on a different remote directory.
112
if relative_url is None:
113
return RemoteTransport(self.base, self)
115
return RemoteTransport(self.abspath(relative_url), self)
117
def is_readonly(self):
118
"""Smart server transport can do read/write file operations."""
119
resp = self._call2('Transport.is_readonly')
120
if resp == ('yes', ):
122
elif resp == ('no', ):
125
self._translate_error(resp)
126
assert False, 'weird response %r' % (resp,)
128
def get_smart_client(self):
131
def get_smart_medium(self):
134
def _unparse_url(self, path):
135
"""Return URL for a path.
137
:see: SFTPUrlHandling._unparse_url
139
# TODO: Eventually it should be possible to unify this with
140
# SFTPUrlHandling._unparse_url?
143
path = urllib.quote(path)
144
netloc = urllib.quote(self._host)
145
if self._username is not None:
146
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
147
if self._port is not None:
148
netloc = '%s:%d' % (netloc, self._port)
149
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
151
def _remote_path(self, relpath):
152
"""Returns the Unicode version of the absolute path for relpath."""
153
return self._combine_paths(self._path, relpath)
155
def _call(self, method, *args):
156
resp = self._call2(method, *args)
157
self._translate_error(resp)
159
def _call2(self, method, *args):
160
"""Call a method on the remote server."""
161
return client._SmartClient(self._medium).call(method, *args)
163
def _call_with_body_bytes(self, method, args, body):
164
"""Call a method on the remote server with body bytes."""
165
smart_client = client._SmartClient(self._medium)
166
return smart_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._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._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')
374
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
376
def disconnect(self):
377
self._medium.disconnect()
379
def delete_tree(self, relpath):
380
raise errors.TransportNotPossible('readonly transport')
382
def stat(self, relpath):
383
resp = self._call2('stat', self._remote_path(relpath))
384
if resp[0] == 'stat':
385
return _SmartStat(int(resp[1]), int(resp[2], 8))
387
self._translate_error(resp)
389
## def lock_read(self, relpath):
390
## """Lock the given file for shared (read) access.
391
## :return: A lock object, which should be passed to Transport.unlock()
393
## # The old RemoteBranch ignore lock for reading, so we will
394
## # continue that tradition and return a bogus lock object.
395
## class BogusLock(object):
396
## def __init__(self, path):
400
## return BogusLock(relpath)
405
def list_dir(self, relpath):
406
resp = self._call2('list_dir', self._remote_path(relpath))
407
if resp[0] == 'names':
408
return [name.encode('ascii') for name in resp[1:]]
410
self._translate_error(resp)
412
def iter_files_recursive(self):
413
resp = self._call2('iter_files_recursive', self._remote_path(''))
414
if resp[0] == 'names':
417
self._translate_error(resp)
420
class RemoteTCPTransport(RemoteTransport):
421
"""Connection to smart server over plain tcp.
423
This is essentially just a factory to get 'RemoteTransport(url,
424
SmartTCPClientMedium).
427
def __init__(self, url):
428
_scheme, _username, _password, _host, _port, _path = \
429
transport.split_url(url)
431
_port = BZR_DEFAULT_PORT
435
except (ValueError, TypeError), e:
436
raise errors.InvalidURL(
437
path=url, extra="invalid port %s" % _port)
438
client_medium = medium.SmartTCPClientMedium(_host, _port)
439
super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
442
class RemoteSSHTransport(RemoteTransport):
443
"""Connection to smart server over SSH.
445
This is essentially just a factory to get 'RemoteTransport(url,
446
SmartSSHClientMedium).
449
def __init__(self, url):
450
_scheme, _username, _password, _host, _port, _path = \
451
transport.split_url(url)
453
if _port is not None:
455
except (ValueError, TypeError), e:
456
raise errors.InvalidURL(path=url, extra="invalid port %s" %
458
client_medium = medium.SmartSSHClientMedium(_host, _port,
459
_username, _password)
460
super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
463
class RemoteHTTPTransport(RemoteTransport):
464
"""Just a way to connect between a bzr+http:// url and http://.
466
This connection operates slightly differently than the RemoteSSHTransport.
467
It uses a plain http:// transport underneath, which defines what remote
468
.bzr/smart URL we are connected to. From there, all paths that are sent are
469
sent as relative paths, this way, the remote side can properly
470
de-reference them, since it is likely doing rewrite rules to translate an
471
HTTP path into a local path.
474
def __init__(self, url, http_transport=None):
475
assert url.startswith('bzr+http://')
477
if http_transport is None:
478
http_url = url[len('bzr+'):]
479
self._http_transport = transport.get_transport(http_url)
481
self._http_transport = http_transport
482
http_medium = self._http_transport.get_smart_medium()
483
super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
485
def _remote_path(self, relpath):
486
"""After connecting HTTP Transport only deals in relative URLs."""
492
def abspath(self, relpath):
493
"""Return the full url to the given relative path.
495
:param relpath: the relative path or path components
496
:type relpath: str or list
498
return self._unparse_url(self._combine_paths(self._path, relpath))
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
508
abs_url = self.abspath(relative_url)
511
# By cloning the underlying http_transport, we are able to share the
513
new_transport = self._http_transport.clone(relative_url)
514
return RemoteHTTPTransport(abs_url, http_transport=new_transport)
517
def get_test_permutations():
518
"""Return (transport, server) permutations for testing."""
519
### We may need a little more test framework support to construct an
520
### appropriate RemoteTransport in the future.
521
from bzrlib.smart import server
522
return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]