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', 'SmartTCPTransport', 'SmartSSHTransport']
25
from cStringIO import StringIO
33
from bzrlib.smart import medium, protocol
35
# must do this otherwise urllib can't parse the urls properly :(
36
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh']:
37
transport.register_urlparse_netloc_protocol(scheme)
41
class _SmartStat(object):
43
def __init__(self, size, mode):
48
class RemoteTransport(transport.Transport):
49
"""Connection to a smart server.
51
The connection holds references to the medium that can be used to send
52
requests to the server.
54
The connection has a notion of the current directory to which it's
55
connected; this is incorporated in filenames passed to the server.
57
This supports some higher-level RPC operations and can also be treated
58
like a Transport to do file-like operations.
60
The connection can be made over a tcp socket, an ssh pipe or a series of
61
http requests. There are concrete subclasses for each type:
62
SmartTCPTransport, etc.
65
# IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
66
# responsibilities: Put those on SmartClient or similar. This is vital for
67
# the ability to support multiple versions of the smart protocol over time:
68
# RemoteTransport is an adapter from the Transport object model to the
69
# SmartClient model, not an encoder.
71
def __init__(self, url, clone_from=None, medium=None):
74
:param medium: The medium to use for this RemoteTransport. This must be
75
supplied if clone_from is None.
77
### Technically super() here is faulty because Transport's __init__
78
### fails to take 2 parameters, and if super were to choose a silly
79
### initialisation order things would blow up.
80
if not url.endswith('/'):
82
super(RemoteTransport, self).__init__(url)
83
self._scheme, self._username, self._password, self._host, self._port, self._path = \
84
transport.split_url(url)
85
if clone_from is None:
88
# credentials may be stripped from the base in some circumstances
89
# as yet to be clearly defined or documented, so copy them.
90
self._username = clone_from._username
91
# reuse same connection
92
self._medium = clone_from._medium
93
assert self._medium is not None
95
def abspath(self, relpath):
96
"""Return the full url to the given relative path.
98
@param relpath: the relative path or path components
99
@type relpath: str or list
101
return self._unparse_url(self._remote_path(relpath))
103
def clone(self, relative_url):
104
"""Make a new RemoteTransport related to me, sharing the same connection.
106
This essentially opens a handle on a different remote directory.
108
if relative_url is None:
109
return RemoteTransport(self.base, self)
111
return RemoteTransport(self.abspath(relative_url), self)
113
def is_readonly(self):
114
"""Smart server transport can do read/write file operations."""
117
def get_smart_client(self):
120
def get_smart_medium(self):
123
def _unparse_url(self, path):
124
"""Return URL for a path.
126
:see: SFTPUrlHandling._unparse_url
128
# TODO: Eventually it should be possible to unify this with
129
# SFTPUrlHandling._unparse_url?
132
path = urllib.quote(path)
133
netloc = urllib.quote(self._host)
134
if self._username is not None:
135
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
136
if self._port is not None:
137
netloc = '%s:%d' % (netloc, self._port)
138
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
140
def _remote_path(self, relpath):
141
"""Returns the Unicode version of the absolute path for relpath."""
142
return self._combine_paths(self._path, relpath)
144
def _call(self, method, *args):
145
resp = self._call2(method, *args)
146
self._translate_error(resp)
148
def _call2(self, method, *args):
149
"""Call a method on the remote server."""
150
request = self._medium.get_request()
151
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
152
smart_protocol.call(method, *args)
153
return smart_protocol.read_response_tuple()
155
def _call_with_body_bytes(self, method, args, body):
156
"""Call a method on the remote server with body bytes."""
157
request = self._medium.get_request()
158
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
159
smart_protocol.call_with_body_bytes((method, ) + args, body)
160
return smart_protocol.read_response_tuple()
162
def has(self, relpath):
163
"""Indicate whether a remote file of the given name exists or not.
165
:see: Transport.has()
167
resp = self._call2('has', self._remote_path(relpath))
168
if resp == ('yes', ):
170
elif resp == ('no', ):
173
self._translate_error(resp)
175
def get(self, relpath):
176
"""Return file-like object reading the contents of a remote file.
178
:see: Transport.get_bytes()/get_file()
180
return StringIO(self.get_bytes(relpath))
182
def get_bytes(self, relpath):
183
remote = self._remote_path(relpath)
184
request = self._medium.get_request()
185
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
186
smart_protocol.call('get', remote)
187
resp = smart_protocol.read_response_tuple(True)
189
smart_protocol.cancel_read_body()
190
self._translate_error(resp, relpath)
191
return smart_protocol.read_body_bytes()
193
def _serialise_optional_mode(self, mode):
199
def mkdir(self, relpath, mode=None):
200
resp = self._call2('mkdir', self._remote_path(relpath),
201
self._serialise_optional_mode(mode))
202
self._translate_error(resp)
204
def put_bytes(self, relpath, upload_contents, mode=None):
205
# FIXME: upload_file is probably not safe for non-ascii characters -
206
# should probably just pass all parameters as length-delimited
208
resp = self._call_with_body_bytes('put',
209
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
211
self._translate_error(resp)
213
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
214
create_parent_dir=False,
216
"""See Transport.put_bytes_non_atomic."""
217
# FIXME: no encoding in the transport!
218
create_parent_str = 'F'
219
if create_parent_dir:
220
create_parent_str = 'T'
222
resp = self._call_with_body_bytes(
224
(self._remote_path(relpath), self._serialise_optional_mode(mode),
225
create_parent_str, self._serialise_optional_mode(dir_mode)),
227
self._translate_error(resp)
229
def put_file(self, relpath, upload_file, mode=None):
230
# its not ideal to seek back, but currently put_non_atomic_file depends
231
# on transports not reading before failing - which is a faulty
232
# assumption I think - RBC 20060915
233
pos = upload_file.tell()
235
return self.put_bytes(relpath, upload_file.read(), mode)
237
upload_file.seek(pos)
240
def put_file_non_atomic(self, relpath, f, mode=None,
241
create_parent_dir=False,
243
return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
244
create_parent_dir=create_parent_dir,
247
def append_file(self, relpath, from_file, mode=None):
248
return self.append_bytes(relpath, from_file.read(), mode)
250
def append_bytes(self, relpath, bytes, mode=None):
251
resp = self._call_with_body_bytes(
253
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
255
if resp[0] == 'appended':
257
self._translate_error(resp)
259
def delete(self, relpath):
260
resp = self._call2('delete', self._remote_path(relpath))
261
self._translate_error(resp)
263
def readv(self, relpath, offsets):
267
offsets = list(offsets)
269
sorted_offsets = sorted(offsets)
270
# turn the list of offsets into a stack
271
offset_stack = iter(offsets)
272
cur_offset_and_size = offset_stack.next()
273
coalesced = list(self._coalesce_offsets(sorted_offsets,
274
limit=self._max_readv_combine,
275
fudge_factor=self._bytes_to_read_before_seek))
277
request = self._medium.get_request()
278
smart_protocol = protocol.SmartClientRequestProtocolOne(request)
279
smart_protocol.call_with_body_readv_array(
280
('readv', self._remote_path(relpath)),
281
[(c.start, c.length) for c in coalesced])
282
resp = smart_protocol.read_response_tuple(True)
284
if resp[0] != 'readv':
285
# This should raise an exception
286
smart_protocol.cancel_read_body()
287
self._translate_error(resp)
290
# FIXME: this should know how many bytes are needed, for clarity.
291
data = smart_protocol.read_body_bytes()
292
# Cache the results, but only until they have been fulfilled
294
for c_offset in coalesced:
295
if len(data) < c_offset.length:
296
raise errors.ShortReadvError(relpath, c_offset.start,
297
c_offset.length, actual=len(data))
298
for suboffset, subsize in c_offset.ranges:
299
key = (c_offset.start+suboffset, subsize)
300
data_map[key] = data[suboffset:suboffset+subsize]
301
data = data[c_offset.length:]
303
# Now that we've read some data, see if we can yield anything back
304
while cur_offset_and_size in data_map:
305
this_data = data_map.pop(cur_offset_and_size)
306
yield cur_offset_and_size[0], this_data
307
cur_offset_and_size = offset_stack.next()
309
def rename(self, rel_from, rel_to):
311
self._remote_path(rel_from),
312
self._remote_path(rel_to))
314
def move(self, rel_from, rel_to):
316
self._remote_path(rel_from),
317
self._remote_path(rel_to))
319
def rmdir(self, relpath):
320
resp = self._call('rmdir', self._remote_path(relpath))
322
def _translate_error(self, resp, orig_path=None):
323
"""Raise an exception from a response"""
330
elif what == 'NoSuchFile':
331
if orig_path is not None:
332
error_path = orig_path
335
raise errors.NoSuchFile(error_path)
336
elif what == 'error':
337
raise errors.SmartProtocolError(unicode(resp[1]))
338
elif what == 'FileExists':
339
raise errors.FileExists(resp[1])
340
elif what == 'DirectoryNotEmpty':
341
raise errors.DirectoryNotEmpty(resp[1])
342
elif what == 'ShortReadvError':
343
raise errors.ShortReadvError(resp[1], int(resp[2]),
344
int(resp[3]), int(resp[4]))
345
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
346
encoding = str(resp[1]) # encoding must always be a string
350
reason = str(resp[5]) # reason must always be a string
351
if val.startswith('u:'):
352
val = val[2:].decode('utf-8')
353
elif val.startswith('s:'):
354
val = val[2:].decode('base64')
355
if what == 'UnicodeDecodeError':
356
raise UnicodeDecodeError(encoding, val, start, end, reason)
357
elif what == 'UnicodeEncodeError':
358
raise UnicodeEncodeError(encoding, val, start, end, reason)
359
elif what == "ReadOnlyError":
360
raise errors.TransportNotPossible('readonly transport')
362
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
364
def disconnect(self):
365
self._medium.disconnect()
367
def delete_tree(self, relpath):
368
raise errors.TransportNotPossible('readonly transport')
370
def stat(self, relpath):
371
resp = self._call2('stat', self._remote_path(relpath))
372
if resp[0] == 'stat':
373
return _SmartStat(int(resp[1]), int(resp[2], 8))
375
self._translate_error(resp)
377
## def lock_read(self, relpath):
378
## """Lock the given file for shared (read) access.
379
## :return: A lock object, which should be passed to Transport.unlock()
381
## # The old RemoteBranch ignore lock for reading, so we will
382
## # continue that tradition and return a bogus lock object.
383
## class BogusLock(object):
384
## def __init__(self, path):
388
## return BogusLock(relpath)
393
def list_dir(self, relpath):
394
resp = self._call2('list_dir', self._remote_path(relpath))
395
if resp[0] == 'names':
396
return [name.encode('ascii') for name in resp[1:]]
398
self._translate_error(resp)
400
def iter_files_recursive(self):
401
resp = self._call2('iter_files_recursive', self._remote_path(''))
402
if resp[0] == 'names':
405
self._translate_error(resp)
408
class SmartTCPTransport(RemoteTransport):
409
"""Connection to smart server over plain tcp.
411
This is essentially just a factory to get 'RemoteTransport(url,
412
SmartTCPClientMedium).
415
def __init__(self, url):
416
_scheme, _username, _password, _host, _port, _path = \
417
transport.split_url(url)
420
except (ValueError, TypeError), e:
421
raise errors.InvalidURL(path=url, extra="invalid port %s" % _port)
422
client_medium = medium.SmartTCPClientMedium(_host, _port)
423
super(SmartTCPTransport, self).__init__(url, medium=client_medium)
426
class SmartSSHTransport(RemoteTransport):
427
"""Connection to smart server over SSH.
429
This is essentially just a factory to get 'RemoteTransport(url,
430
SmartSSHClientMedium).
433
def __init__(self, url):
434
_scheme, _username, _password, _host, _port, _path = \
435
transport.split_url(url)
437
if _port is not None:
439
except (ValueError, TypeError), e:
440
raise errors.InvalidURL(path=url, extra="invalid port %s" %
442
client_medium = medium.SmartSSHClientMedium(_host, _port,
443
_username, _password)
444
super(SmartSSHTransport, self).__init__(url, medium=client_medium)
447
def get_test_permutations():
448
"""Return (transport, server) permutations for testing."""
449
### We may need a little more test framework support to construct an
450
### appropriate RemoteTransport in the future.
451
from bzrlib.smart import server
452
return [(SmartTCPTransport, server.SmartTCPServer_for_testing)]