74
68
# RemoteTransport is an adapter from the Transport object model to the
75
69
# SmartClient model, not an encoder.
77
def __init__(self, url, clone_from=None, medium=None, _client=None):
71
# FIXME: the medium parameter should be private, only the tests requires
72
# it. It may be even clearer to define a TestRemoteTransport that handles
73
# the specific cases of providing a _client and/or a _medium, and leave
74
# RemoteTransport as an abstract class.
75
def __init__(self, url, _from_transport=None, medium=None, _client=None):
80
:param clone_from: Another RemoteTransport instance that this one is
81
being cloned from. Attributes such as credentials and the medium
78
:param _from_transport: Another RemoteTransport instance that this
79
one is being cloned from. Attributes such as the medium will
83
82
:param medium: The medium to use for this RemoteTransport. This must be
84
supplied if clone_from is None.
83
supplied if _from_transport is None.
85
85
:param _client: Override the _SmartClient used by this transport. This
86
86
should only be used for testing purposes; normally this is
87
87
determined from the medium.
89
### Technically super() here is faulty because Transport's __init__
90
### fails to take 2 parameters, and if super were to choose a silly
91
### initialisation order things would blow up.
92
if not url.endswith('/'):
94
super(RemoteTransport, self).__init__(url)
95
self._scheme, self._username, self._password, self._host, self._port, self._path = \
96
transport.split_url(url)
97
if clone_from is None:
89
super(RemoteTransport, self).__init__(url,
90
_from_transport=_from_transport)
92
# The medium is the connection, except when we need to share it with
93
# other objects (RemoteBzrDir, RemoteRepository etc). In these cases
94
# what we want to share is really the shared connection.
96
if _from_transport is None:
97
# If no _from_transport is specified, we need to intialize the
101
medium, credentials = self._build_medium()
102
if 'hpss' in debug.debug_flags:
103
trace.mutter('hpss: Built a new medium: %s',
104
medium.__class__.__name__)
105
self._shared_connection = transport._SharedConnection(medium,
100
# credentials may be stripped from the base in some circumstances
101
# as yet to be clearly defined or documented, so copy them.
102
self._username = clone_from._username
103
# reuse same connection
104
self._medium = clone_from._medium
105
assert self._medium is not None
110
# No medium was specified, so share the medium from the
112
medium = self._shared_connection.connection
106
114
if _client is None:
107
self._client = client._SmartClient(self._medium)
115
self._client = client._SmartClient(medium, self.base)
109
117
self._client = _client
111
def abspath(self, relpath):
112
"""Return the full url to the given relative path.
114
@param relpath: the relative path or path components
115
@type relpath: str or list
117
return self._unparse_url(self._remote_path(relpath))
119
def clone(self, relative_url):
120
"""Make a new RemoteTransport related to me, sharing the same connection.
119
def _build_medium(self):
120
"""Create the medium if _from_transport does not provide one.
122
This essentially opens a handle on a different remote directory.
122
The medium is analogous to the connection for ConnectedTransport: it
123
allows connection sharing.
124
if relative_url is None:
125
return RemoteTransport(self.base, self)
127
return RemoteTransport(self.abspath(relative_url), self)
129
128
def is_readonly(self):
130
129
"""Smart server transport can do read/write file operations."""
134
133
elif resp == ('no', ):
136
elif resp == ('error', "Generic bzr smart protocol error: "
137
"bad request 'Transport.is_readonly'"):
135
elif (resp == ('error', "Generic bzr smart protocol error: "
136
"bad request 'Transport.is_readonly'") or
137
resp == ('error', "Generic bzr smart protocol error: "
138
"bad request u'Transport.is_readonly'")):
138
139
# XXX: nasty hack: servers before 0.16 don't have a
139
140
# 'Transport.is_readonly' verb, so we do what clients before 0.16
140
141
# did: assume False.
143
144
self._translate_error(resp)
144
assert False, 'weird response %r' % (resp,)
145
raise errors.UnexpectedSmartServerResponse(resp)
146
147
def get_smart_client(self):
148
return self._get_connection()
149
150
def get_smart_medium(self):
152
def _unparse_url(self, path):
153
"""Return URL for a path.
151
return self._get_connection()
155
:see: SFTPUrlHandling._unparse_url
157
# TODO: Eventually it should be possible to unify this with
158
# SFTPUrlHandling._unparse_url?
161
path = urllib.quote(path)
162
netloc = urllib.quote(self._host)
163
if self._username is not None:
164
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
165
if self._port is not None:
166
netloc = '%s:%d' % (netloc, self._port)
167
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
153
@deprecated_method(one_four)
154
def get_shared_medium(self):
155
return self._get_shared_connection()
169
157
def _remote_path(self, relpath):
170
158
"""Returns the Unicode version of the absolute path for relpath."""
224
212
self._serialise_optional_mode(mode))
225
213
self._translate_error(resp)
215
def open_write_stream(self, relpath, mode=None):
216
"""See Transport.open_write_stream."""
217
self.put_bytes(relpath, "", mode)
218
result = transport.AppendBasedFileStream(self, relpath)
219
transport._file_streams[self.abspath(relpath)] = result
227
222
def put_bytes(self, relpath, upload_contents, mode=None):
228
223
# FIXME: upload_file is probably not safe for non-ascii characters -
229
224
# should probably just pass all parameters as length-delimited
387
392
raise UnicodeEncodeError(encoding, val, start, end, reason)
388
393
elif what == "ReadOnlyError":
389
394
raise errors.TransportNotPossible('readonly transport')
395
elif what == "ReadError":
396
if orig_path is not None:
397
error_path = orig_path
400
raise errors.ReadError(error_path)
401
elif what == "PermissionDenied":
402
if orig_path is not None:
403
error_path = orig_path
406
raise errors.PermissionDenied(error_path)
391
408
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
393
410
def disconnect(self):
394
self._medium.disconnect()
411
self.get_smart_medium().disconnect()
396
413
def delete_tree(self, relpath):
397
414
raise errors.TransportNotPossible('readonly transport')
441
458
SmartTCPClientMedium).
444
def __init__(self, url):
445
_scheme, _username, _password, _host, _port, _path = \
446
transport.split_url(url)
448
_port = BZR_DEFAULT_PORT
452
except (ValueError, TypeError), e:
453
raise errors.InvalidURL(
454
path=url, extra="invalid port %s" % _port)
455
client_medium = medium.SmartTCPClientMedium(_host, _port)
456
super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
461
def _build_medium(self):
462
assert self.base.startswith('bzr://')
463
return medium.SmartTCPClientMedium(self._host, self._port), None
459
466
class RemoteSSHTransport(RemoteTransport):
463
470
SmartSSHClientMedium).
466
def __init__(self, url):
467
_scheme, _username, _password, _host, _port, _path = \
468
transport.split_url(url)
470
if _port is not None:
472
except (ValueError, TypeError), e:
473
raise errors.InvalidURL(path=url, extra="invalid port %s" %
475
client_medium = medium.SmartSSHClientMedium(_host, _port,
476
_username, _password)
477
super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
473
def _build_medium(self):
474
assert self.base.startswith('bzr+ssh://')
475
# ssh will prompt the user for a password if needed and if none is
476
# provided but it will not give it back, so no credentials can be
478
location_config = config.LocationConfig(self.base)
479
bzr_remote_path = location_config.get_bzr_remote_path()
480
return medium.SmartSSHClientMedium(self._host, self._port,
481
self._user, self._password, bzr_remote_path=bzr_remote_path), None
480
484
class RemoteHTTPTransport(RemoteTransport):
488
492
HTTP path into a local path.
491
def __init__(self, url, http_transport=None):
492
assert url.startswith('bzr+http://')
495
def __init__(self, base, _from_transport=None, http_transport=None):
496
assert ( base.startswith('bzr+http://') or base.startswith('bzr+https://') )
494
498
if http_transport is None:
495
http_url = url[len('bzr+'):]
499
# FIXME: the password may be lost here because it appears in the
500
# url only for an intial construction (when the url came from the
502
http_url = base[len('bzr+'):]
496
503
self._http_transport = transport.get_transport(http_url)
498
505
self._http_transport = http_transport
499
http_medium = self._http_transport.get_smart_medium()
500
super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
506
super(RemoteHTTPTransport, self).__init__(
507
base, _from_transport=_from_transport)
509
def _build_medium(self):
510
# We let http_transport take care of the credentials
511
return self._http_transport.get_smart_medium(), None
502
513
def _remote_path(self, relpath):
503
"""After connecting HTTP Transport only deals in relative URLs."""
514
"""After connecting, HTTP Transport only deals in relative URLs."""
504
515
# Adjust the relpath based on which URL this smart transport is
506
base = urlutils.normalize_url(self._http_transport.base)
517
http_base = urlutils.normalize_url(self.get_smart_medium().base)
507
518
url = urlutils.join(self.base[len('bzr+'):], relpath)
508
519
url = urlutils.normalize_url(url)
509
return urlutils.relative_url(base, url)
511
def abspath(self, relpath):
512
"""Return the full url to the given relative path.
514
:param relpath: the relative path or path components
515
:type relpath: str or list
517
return self._unparse_url(self._combine_paths(self._path, relpath))
520
return urlutils.relative_url(http_base, url)
519
522
def clone(self, relative_url):
520
523
"""Make a new RemoteHTTPTransport related to me.
528
531
smart requests may be different). This is so that the server doesn't
529
532
have to handle .bzr/smart requests at arbitrary places inside .bzr
530
533
directories, just at the initial URL the user uses.
532
The exception is parent paths (i.e. relative_url of "..").
535
536
abs_url = self.abspath(relative_url)
537
538
abs_url = self.base
538
# We either use the exact same http_transport (for child locations), or
539
# a clone of the underlying http_transport (for parent locations). This
540
# means we share the connection.
541
norm_base = urlutils.normalize_url(self.base)
542
norm_abs_url = urlutils.normalize_url(abs_url)
543
normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
544
if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
545
http_transport = self._http_transport.clone(normalized_rel_url)
547
http_transport = self._http_transport
548
return RemoteHTTPTransport(abs_url, http_transport=http_transport)
539
return RemoteHTTPTransport(abs_url,
540
_from_transport=self,
541
http_transport=self._http_transport)
551
544
def get_test_permutations():