/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Robert Collins
  • Date: 2007-05-07 16:48:14 UTC
  • mto: This revision was merged to the branch mainline in revision 2485.
  • Revision ID: robertc@robertcollins.net-20070507164814-wpagonutf4b5cf8s
Move HACKING to docs/developers/HACKING and adjust Makefile to accomodate this.

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""RemoteTransport client for the smart-server.
18
18
 
23
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
24
24
 
25
25
from cStringIO import StringIO
 
26
import urllib
 
27
import urlparse
26
28
 
27
29
from bzrlib import (
28
 
    config,
29
 
    debug,
30
30
    errors,
31
 
    remote,
32
 
    trace,
33
31
    transport,
34
32
    urlutils,
35
33
    )
36
 
from bzrlib.smart import client, medium
37
 
from bzrlib.symbol_versioning import (
38
 
    deprecated_method,
39
 
    )
 
34
from bzrlib.smart import client, medium, protocol
 
35
 
 
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)
 
39
del scheme
 
40
 
 
41
 
 
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
40
45
 
41
46
 
42
47
class _SmartStat(object):
46
51
        self.st_mode = mode
47
52
 
48
53
 
49
 
class RemoteTransport(transport.ConnectedTransport):
 
54
class RemoteTransport(transport.Transport):
50
55
    """Connection to a smart server.
51
56
 
52
57
    The connection holds references to the medium that can be used to send
54
59
 
55
60
    The connection has a notion of the current directory to which it's
56
61
    connected; this is incorporated in filenames passed to the server.
57
 
 
58
 
    This supports some higher-level RPC operations and can also be treated
 
62
    
 
63
    This supports some higher-level RPC operations and can also be treated 
59
64
    like a Transport to do file-like operations.
60
65
 
61
66
    The connection can be made over a tcp socket, an ssh pipe or a series of
63
68
    RemoteTCPTransport, etc.
64
69
    """
65
70
 
66
 
    # When making a readv request, cap it at requesting 5MB of data
67
 
    _max_readv_bytes = 5*1024*1024
68
 
 
69
71
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
70
72
    # responsibilities: Put those on SmartClient or similar. This is vital for
71
73
    # the ability to support multiple versions of the smart protocol over time:
72
 
    # RemoteTransport is an adapter from the Transport object model to the
 
74
    # RemoteTransport is an adapter from the Transport object model to the 
73
75
    # SmartClient model, not an encoder.
74
76
 
75
 
    # FIXME: the medium parameter should be private, only the tests requires
76
 
    # it. It may be even clearer to define a TestRemoteTransport that handles
77
 
    # the specific cases of providing a _client and/or a _medium, and leave
78
 
    # RemoteTransport as an abstract class.
79
 
    def __init__(self, url, _from_transport=None, medium=None, _client=None):
 
77
    def __init__(self, url, clone_from=None, medium=None, _client=None):
80
78
        """Constructor.
81
79
 
82
 
        :param _from_transport: Another RemoteTransport instance that this
83
 
            one is being cloned from.  Attributes such as the medium will
84
 
            be reused.
85
 
 
86
 
        :param medium: The medium to use for this RemoteTransport.  If None,
87
 
            the medium from the _from_transport is shared.  If both this
88
 
            and _from_transport are None, a new medium will be built.
89
 
            _from_transport and medium cannot both be specified.
90
 
 
 
80
        :param clone_from: Another RemoteTransport instance that this one is
 
81
            being cloned from.  Attributes such as credentials and the medium
 
82
            will be reused.
 
83
        :param medium: The medium to use for this RemoteTransport. This must be
 
84
            supplied if clone_from is None.
91
85
        :param _client: Override the _SmartClient used by this transport.  This
92
86
            should only be used for testing purposes; normally this is
93
87
            determined from the medium.
94
88
        """
95
 
        super(RemoteTransport, self).__init__(
96
 
            url, _from_transport=_from_transport)
97
 
 
98
 
        # The medium is the connection, except when we need to share it with
99
 
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
 
        # what we want to share is really the shared connection.
101
 
 
102
 
        if (_from_transport is not None
103
 
            and isinstance(_from_transport, RemoteTransport)):
104
 
            _client = _from_transport._client
105
 
        elif _from_transport is None:
106
 
            # If no _from_transport is specified, we need to intialize the
107
 
            # shared medium.
108
 
            credentials = None
109
 
            if medium is None:
110
 
                medium, credentials = self._build_medium()
111
 
                if 'hpss' in debug.debug_flags:
112
 
                    trace.mutter('hpss: Built a new medium: %s',
113
 
                                 medium.__class__.__name__)
114
 
            self._shared_connection = transport._SharedConnection(medium,
115
 
                                                                  credentials,
116
 
                                                                  self.base)
117
 
        elif medium is None:
118
 
            # No medium was specified, so share the medium from the
119
 
            # _from_transport.
120
 
            medium = self._shared_connection.connection
 
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('/'):
 
93
            url += '/'
 
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:
 
98
            self._medium = medium
121
99
        else:
122
 
            raise AssertionError(
123
 
                "Both _from_transport (%r) and medium (%r) passed to "
124
 
                "RemoteTransport.__init__, but these parameters are mutally "
125
 
                "exclusive." % (_from_transport, medium))
126
 
 
 
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
127
106
        if _client is None:
128
 
            self._client = client._SmartClient(medium)
 
107
            self._client = client._SmartClient(self._medium)
129
108
        else:
130
109
            self._client = _client
131
110
 
132
 
    def _build_medium(self):
133
 
        """Create the medium if _from_transport does not provide one.
134
 
 
135
 
        The medium is analogous to the connection for ConnectedTransport: it
136
 
        allows connection sharing.
137
 
        """
138
 
        # No credentials
139
 
        return None, None
140
 
 
141
 
    def _report_activity(self, bytes, direction):
142
 
        """See Transport._report_activity.
143
 
 
144
 
        Does nothing; the smart medium will report activity triggered by a
145
 
        RemoteTransport.
146
 
        """
147
 
        pass
 
111
    def abspath(self, relpath):
 
112
        """Return the full url to the given relative path.
 
113
        
 
114
        @param relpath: the relative path or path components
 
115
        @type relpath: str or list
 
116
        """
 
117
        return self._unparse_url(self._remote_path(relpath))
 
118
    
 
119
    def clone(self, relative_url):
 
120
        """Make a new RemoteTransport related to me, sharing the same connection.
 
121
 
 
122
        This essentially opens a handle on a different remote directory.
 
123
        """
 
124
        if relative_url is None:
 
125
            return RemoteTransport(self.base, self)
 
126
        else:
 
127
            return RemoteTransport(self.abspath(relative_url), self)
148
128
 
149
129
    def is_readonly(self):
150
130
        """Smart server transport can do read/write file operations."""
151
 
        try:
152
 
            resp = self._call2('Transport.is_readonly')
153
 
        except errors.UnknownSmartMethod:
 
131
        resp = self._call2('Transport.is_readonly')
 
132
        if resp == ('yes', ):
 
133
            return True
 
134
        elif resp == ('no', ):
 
135
            return False
 
136
        elif resp == ('error', "Generic bzr smart protocol error: "
 
137
                               "bad request 'Transport.is_readonly'"):
154
138
            # XXX: nasty hack: servers before 0.16 don't have a
155
139
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
156
140
            # did: assume False.
157
141
            return False
158
 
        if resp == ('yes', ):
159
 
            return True
160
 
        elif resp == ('no', ):
161
 
            return False
162
142
        else:
163
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
143
            self._translate_error(resp)
 
144
        assert False, 'weird response %r' % (resp,)
164
145
 
165
146
    def get_smart_client(self):
166
 
        return self._get_connection()
 
147
        return self._medium
167
148
 
168
149
    def get_smart_medium(self):
169
 
        return self._get_connection()
 
150
        return self._medium
 
151
                                                   
 
152
    def _unparse_url(self, path):
 
153
        """Return URL for a path.
 
154
 
 
155
        :see: SFTPUrlHandling._unparse_url
 
156
        """
 
157
        # TODO: Eventually it should be possible to unify this with
 
158
        # SFTPUrlHandling._unparse_url?
 
159
        if path == '':
 
160
            path = '/'
 
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, '', '', ''))
170
168
 
171
169
    def _remote_path(self, relpath):
172
170
        """Returns the Unicode version of the absolute path for relpath."""
174
172
 
175
173
    def _call(self, method, *args):
176
174
        resp = self._call2(method, *args)
177
 
        self._ensure_ok(resp)
 
175
        self._translate_error(resp)
178
176
 
179
177
    def _call2(self, method, *args):
180
178
        """Call a method on the remote server."""
181
 
        try:
182
 
            return self._client.call(method, *args)
183
 
        except errors.ErrorFromSmartServer, err:
184
 
            # The first argument, if present, is always a path.
185
 
            if args:
186
 
                context = {'relpath': args[0]}
187
 
            else:
188
 
                context = {}
189
 
            self._translate_error(err, **context)
 
179
        return self._client.call(method, *args)
190
180
 
191
181
    def _call_with_body_bytes(self, method, args, body):
192
182
        """Call a method on the remote server with body bytes."""
193
 
        try:
194
 
            return self._client.call_with_body_bytes(method, args, body)
195
 
        except errors.ErrorFromSmartServer, err:
196
 
            # The first argument, if present, is always a path.
197
 
            if args:
198
 
                context = {'relpath': args[0]}
199
 
            else:
200
 
                context = {}
201
 
            self._translate_error(err, **context)
 
183
        return self._client.call_with_body_bytes(method, args, body)
202
184
 
203
185
    def has(self, relpath):
204
186
        """Indicate whether a remote file of the given name exists or not.
211
193
        elif resp == ('no', ):
212
194
            return False
213
195
        else:
214
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
196
            self._translate_error(resp)
215
197
 
216
198
    def get(self, relpath):
217
199
        """Return file-like object reading the contents of a remote file.
218
 
 
 
200
        
219
201
        :see: Transport.get_bytes()/get_file()
220
202
        """
221
203
        return StringIO(self.get_bytes(relpath))
222
204
 
223
205
    def get_bytes(self, relpath):
224
206
        remote = self._remote_path(relpath)
225
 
        try:
226
 
            resp, response_handler = self._client.call_expecting_body('get', remote)
227
 
        except errors.ErrorFromSmartServer, err:
228
 
            self._translate_error(err, relpath)
 
207
        request = self._medium.get_request()
 
208
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
209
        smart_protocol.call('get', remote)
 
210
        resp = smart_protocol.read_response_tuple(True)
229
211
        if resp != ('ok', ):
230
 
            response_handler.cancel_read_body()
231
 
            raise errors.UnexpectedSmartServerResponse(resp)
232
 
        return response_handler.read_body_bytes()
 
212
            smart_protocol.cancel_read_body()
 
213
            self._translate_error(resp, relpath)
 
214
        return smart_protocol.read_body_bytes()
233
215
 
234
216
    def _serialise_optional_mode(self, mode):
235
217
        if mode is None:
240
222
    def mkdir(self, relpath, mode=None):
241
223
        resp = self._call2('mkdir', self._remote_path(relpath),
242
224
            self._serialise_optional_mode(mode))
243
 
 
244
 
    def open_write_stream(self, relpath, mode=None):
245
 
        """See Transport.open_write_stream."""
246
 
        self.put_bytes(relpath, "", mode)
247
 
        result = transport.AppendBasedFileStream(self, relpath)
248
 
        transport._file_streams[self.abspath(relpath)] = result
249
 
        return result
 
225
        self._translate_error(resp)
250
226
 
251
227
    def put_bytes(self, relpath, upload_contents, mode=None):
252
228
        # FIXME: upload_file is probably not safe for non-ascii characters -
261
237
        resp = self._call_with_body_bytes('put',
262
238
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
263
239
            upload_contents)
264
 
        self._ensure_ok(resp)
265
 
        return len(upload_contents)
 
240
        self._translate_error(resp)
266
241
 
267
242
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
268
243
                             create_parent_dir=False,
278
253
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
279
254
             create_parent_str, self._serialise_optional_mode(dir_mode)),
280
255
            bytes)
281
 
        self._ensure_ok(resp)
 
256
        self._translate_error(resp)
282
257
 
283
258
    def put_file(self, relpath, upload_file, mode=None):
284
259
        # its not ideal to seek back, but currently put_non_atomic_file depends
300
275
 
301
276
    def append_file(self, relpath, from_file, mode=None):
302
277
        return self.append_bytes(relpath, from_file.read(), mode)
303
 
 
 
278
        
304
279
    def append_bytes(self, relpath, bytes, mode=None):
305
280
        resp = self._call_with_body_bytes(
306
281
            'append',
308
283
            bytes)
309
284
        if resp[0] == 'appended':
310
285
            return int(resp[1])
311
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
286
        self._translate_error(resp)
312
287
 
313
288
    def delete(self, relpath):
314
289
        resp = self._call2('delete', self._remote_path(relpath))
315
 
        self._ensure_ok(resp)
316
 
 
317
 
    def external_url(self):
318
 
        """See bzrlib.transport.Transport.external_url."""
319
 
        # the external path for RemoteTransports is the base
320
 
        return self.base
321
 
 
322
 
    def recommended_page_size(self):
323
 
        """Return the recommended page size for this transport."""
324
 
        return 64 * 1024
325
 
 
326
 
    def _readv(self, relpath, offsets):
 
290
        self._translate_error(resp)
 
291
 
 
292
    def readv(self, relpath, offsets):
327
293
        if not offsets:
328
294
            return
329
295
 
330
296
        offsets = list(offsets)
331
297
 
332
298
        sorted_offsets = sorted(offsets)
 
299
        # turn the list of offsets into a stack
 
300
        offset_stack = iter(offsets)
 
301
        cur_offset_and_size = offset_stack.next()
333
302
        coalesced = list(self._coalesce_offsets(sorted_offsets,
334
303
                               limit=self._max_readv_combine,
335
 
                               fudge_factor=self._bytes_to_read_before_seek,
336
 
                               max_size=self._max_readv_bytes))
337
 
 
338
 
        # now that we've coallesced things, avoid making enormous requests
339
 
        requests = []
340
 
        cur_request = []
341
 
        cur_len = 0
342
 
        for c in coalesced:
343
 
            if c.length + cur_len > self._max_readv_bytes:
344
 
                requests.append(cur_request)
345
 
                cur_request = [c]
346
 
                cur_len = c.length
347
 
                continue
348
 
            cur_request.append(c)
349
 
            cur_len += c.length
350
 
        if cur_request:
351
 
            requests.append(cur_request)
352
 
        if 'hpss' in debug.debug_flags:
353
 
            trace.mutter('%s.readv %s offsets => %s coalesced'
354
 
                         ' => %s requests (%s)',
355
 
                         self.__class__.__name__, len(offsets), len(coalesced),
356
 
                         len(requests), sum(map(len, requests)))
 
304
                               fudge_factor=self._bytes_to_read_before_seek))
 
305
 
 
306
        request = self._medium.get_request()
 
307
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
308
        smart_protocol.call_with_body_readv_array(
 
309
            ('readv', self._remote_path(relpath)),
 
310
            [(c.start, c.length) for c in coalesced])
 
311
        resp = smart_protocol.read_response_tuple(True)
 
312
 
 
313
        if resp[0] != 'readv':
 
314
            # This should raise an exception
 
315
            smart_protocol.cancel_read_body()
 
316
            self._translate_error(resp)
 
317
            return
 
318
 
 
319
        # FIXME: this should know how many bytes are needed, for clarity.
 
320
        data = smart_protocol.read_body_bytes()
357
321
        # Cache the results, but only until they have been fulfilled
358
322
        data_map = {}
359
 
        # turn the list of offsets into a single stack to iterate
360
 
        offset_stack = iter(offsets)
361
 
        # using a list so it can be modified when passing down and coming back
362
 
        next_offset = [offset_stack.next()]
363
 
        for cur_request in requests:
364
 
            try:
365
 
                result = self._client.call_with_body_readv_array(
366
 
                    ('readv', self._remote_path(relpath),),
367
 
                    [(c.start, c.length) for c in cur_request])
368
 
                resp, response_handler = result
369
 
            except errors.ErrorFromSmartServer, err:
370
 
                self._translate_error(err, relpath)
371
 
 
372
 
            if resp[0] != 'readv':
373
 
                # This should raise an exception
374
 
                response_handler.cancel_read_body()
375
 
                raise errors.UnexpectedSmartServerResponse(resp)
376
 
 
377
 
            for res in self._handle_response(offset_stack, cur_request,
378
 
                                             response_handler,
379
 
                                             data_map,
380
 
                                             next_offset):
381
 
                yield res
382
 
 
383
 
    def _handle_response(self, offset_stack, coalesced, response_handler,
384
 
                         data_map, next_offset):
385
 
        cur_offset_and_size = next_offset[0]
386
 
        # FIXME: this should know how many bytes are needed, for clarity.
387
 
        data = response_handler.read_body_bytes()
388
 
        data_offset = 0
389
323
        for c_offset in coalesced:
390
324
            if len(data) < c_offset.length:
391
325
                raise errors.ShortReadvError(relpath, c_offset.start,
392
326
                            c_offset.length, actual=len(data))
393
327
            for suboffset, subsize in c_offset.ranges:
394
328
                key = (c_offset.start+suboffset, subsize)
395
 
                this_data = data[data_offset+suboffset:
396
 
                                 data_offset+suboffset+subsize]
397
 
                # Special case when the data is in-order, rather than packing
398
 
                # into a map and then back out again. Benchmarking shows that
399
 
                # this has 100% hit rate, but leave in the data_map work just
400
 
                # in case.
401
 
                # TODO: Could we get away with using buffer() to avoid the
402
 
                #       memory copy?  Callers would need to realize they may
403
 
                #       not have a real string.
404
 
                if key == cur_offset_and_size:
405
 
                    yield cur_offset_and_size[0], this_data
406
 
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
407
 
                else:
408
 
                    data_map[key] = this_data
409
 
            data_offset += c_offset.length
 
329
                data_map[key] = data[suboffset:suboffset+subsize]
 
330
            data = data[c_offset.length:]
410
331
 
411
332
            # Now that we've read some data, see if we can yield anything back
412
333
            while cur_offset_and_size in data_map:
413
334
                this_data = data_map.pop(cur_offset_and_size)
414
335
                yield cur_offset_and_size[0], this_data
415
 
                cur_offset_and_size = next_offset[0] = offset_stack.next()
 
336
                cur_offset_and_size = offset_stack.next()
416
337
 
417
338
    def rename(self, rel_from, rel_to):
418
339
        self._call('rename',
427
348
    def rmdir(self, relpath):
428
349
        resp = self._call('rmdir', self._remote_path(relpath))
429
350
 
430
 
    def _ensure_ok(self, resp):
431
 
        if resp[0] != 'ok':
432
 
            raise errors.UnexpectedSmartServerResponse(resp)
433
 
 
434
 
    def _translate_error(self, err, relpath=None):
435
 
        remote._translate_error(err, path=relpath)
 
351
    def _translate_error(self, resp, orig_path=None):
 
352
        """Raise an exception from a response"""
 
353
        if resp is None:
 
354
            what = None
 
355
        else:
 
356
            what = resp[0]
 
357
        if what == 'ok':
 
358
            return
 
359
        elif what == 'NoSuchFile':
 
360
            if orig_path is not None:
 
361
                error_path = orig_path
 
362
            else:
 
363
                error_path = resp[1]
 
364
            raise errors.NoSuchFile(error_path)
 
365
        elif what == 'error':
 
366
            raise errors.SmartProtocolError(unicode(resp[1]))
 
367
        elif what == 'FileExists':
 
368
            raise errors.FileExists(resp[1])
 
369
        elif what == 'DirectoryNotEmpty':
 
370
            raise errors.DirectoryNotEmpty(resp[1])
 
371
        elif what == 'ShortReadvError':
 
372
            raise errors.ShortReadvError(resp[1], int(resp[2]),
 
373
                                         int(resp[3]), int(resp[4]))
 
374
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
 
375
            encoding = str(resp[1]) # encoding must always be a string
 
376
            val = resp[2]
 
377
            start = int(resp[3])
 
378
            end = int(resp[4])
 
379
            reason = str(resp[5]) # reason must always be a string
 
380
            if val.startswith('u:'):
 
381
                val = val[2:].decode('utf-8')
 
382
            elif val.startswith('s:'):
 
383
                val = val[2:].decode('base64')
 
384
            if what == 'UnicodeDecodeError':
 
385
                raise UnicodeDecodeError(encoding, val, start, end, reason)
 
386
            elif what == 'UnicodeEncodeError':
 
387
                raise UnicodeEncodeError(encoding, val, start, end, reason)
 
388
        elif what == "ReadOnlyError":
 
389
            raise errors.TransportNotPossible('readonly transport')
 
390
        else:
 
391
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
436
392
 
437
393
    def disconnect(self):
438
 
        self.get_smart_medium().disconnect()
 
394
        self._medium.disconnect()
 
395
 
 
396
    def delete_tree(self, relpath):
 
397
        raise errors.TransportNotPossible('readonly transport')
439
398
 
440
399
    def stat(self, relpath):
441
400
        resp = self._call2('stat', self._remote_path(relpath))
442
401
        if resp[0] == 'stat':
443
402
            return _SmartStat(int(resp[1]), int(resp[2], 8))
444
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
403
        else:
 
404
            self._translate_error(resp)
445
405
 
446
406
    ## def lock_read(self, relpath):
447
407
    ##     """Lock the given file for shared (read) access.
463
423
        resp = self._call2('list_dir', self._remote_path(relpath))
464
424
        if resp[0] == 'names':
465
425
            return [name.encode('ascii') for name in resp[1:]]
466
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
426
        else:
 
427
            self._translate_error(resp)
467
428
 
468
429
    def iter_files_recursive(self):
469
430
        resp = self._call2('iter_files_recursive', self._remote_path(''))
470
431
        if resp[0] == 'names':
471
432
            return resp[1:]
472
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
433
        else:
 
434
            self._translate_error(resp)
473
435
 
474
436
 
475
437
class RemoteTCPTransport(RemoteTransport):
476
438
    """Connection to smart server over plain tcp.
477
 
 
 
439
    
478
440
    This is essentially just a factory to get 'RemoteTransport(url,
479
441
        SmartTCPClientMedium).
480
442
    """
481
443
 
482
 
    def _build_medium(self):
483
 
        client_medium = medium.SmartTCPClientMedium(
484
 
            self._host, self._port, self.base)
485
 
        return client_medium, None
486
 
 
487
 
 
488
 
class RemoteTCPTransportV2Only(RemoteTransport):
489
 
    """Connection to smart server over plain tcp with the client hard-coded to
490
 
    assume protocol v2 and remote server version <= 1.6.
491
 
 
492
 
    This should only be used for testing.
493
 
    """
494
 
 
495
 
    def _build_medium(self):
496
 
        client_medium = medium.SmartTCPClientMedium(
497
 
            self._host, self._port, self.base)
498
 
        client_medium._protocol_version = 2
499
 
        client_medium._remember_remote_is_before((1, 6))
500
 
        return client_medium, None
 
444
    def __init__(self, url):
 
445
        _scheme, _username, _password, _host, _port, _path = \
 
446
            transport.split_url(url)
 
447
        if _port is None:
 
448
            _port = BZR_DEFAULT_PORT
 
449
        else:
 
450
            try:
 
451
                _port = int(_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)
501
457
 
502
458
 
503
459
class RemoteSSHTransport(RemoteTransport):
507
463
        SmartSSHClientMedium).
508
464
    """
509
465
 
510
 
    def _build_medium(self):
511
 
        location_config = config.LocationConfig(self.base)
512
 
        bzr_remote_path = location_config.get_bzr_remote_path()
513
 
        user = self._user
514
 
        if user is None:
515
 
            auth = config.AuthenticationConfig()
516
 
            user = auth.get_user('ssh', self._host, self._port)
517
 
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
518
 
            user, self._password, self.base,
519
 
            bzr_remote_path=bzr_remote_path)
520
 
        return client_medium, (user, self._password)
 
466
    def __init__(self, url):
 
467
        _scheme, _username, _password, _host, _port, _path = \
 
468
            transport.split_url(url)
 
469
        try:
 
470
            if _port is not None:
 
471
                _port = int(_port)
 
472
        except (ValueError, TypeError), e:
 
473
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
 
474
                _port)
 
475
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
476
                                                    _username, _password)
 
477
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
521
478
 
522
479
 
523
480
class RemoteHTTPTransport(RemoteTransport):
524
481
    """Just a way to connect between a bzr+http:// url and http://.
525
 
 
 
482
    
526
483
    This connection operates slightly differently than the RemoteSSHTransport.
527
484
    It uses a plain http:// transport underneath, which defines what remote
528
485
    .bzr/smart URL we are connected to. From there, all paths that are sent are
531
488
    HTTP path into a local path.
532
489
    """
533
490
 
534
 
    def __init__(self, base, _from_transport=None, http_transport=None):
 
491
    def __init__(self, url, http_transport=None):
 
492
        assert url.startswith('bzr+http://')
 
493
 
535
494
        if http_transport is None:
536
 
            # FIXME: the password may be lost here because it appears in the
537
 
            # url only for an intial construction (when the url came from the
538
 
            # command-line).
539
 
            http_url = base[len('bzr+'):]
 
495
            http_url = url[len('bzr+'):]
540
496
            self._http_transport = transport.get_transport(http_url)
541
497
        else:
542
498
            self._http_transport = http_transport
543
 
        super(RemoteHTTPTransport, self).__init__(
544
 
            base, _from_transport=_from_transport)
545
 
 
546
 
    def _build_medium(self):
547
 
        # We let http_transport take care of the credentials
548
 
        return self._http_transport.get_smart_medium(), None
 
499
        http_medium = self._http_transport.get_smart_medium()
 
500
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
549
501
 
550
502
    def _remote_path(self, relpath):
551
 
        """After connecting, HTTP Transport only deals in relative URLs."""
 
503
        """After connecting HTTP Transport only deals in relative URLs."""
552
504
        # Adjust the relpath based on which URL this smart transport is
553
505
        # connected to.
554
 
        http_base = urlutils.normalize_url(self.get_smart_medium().base)
 
506
        base = urlutils.normalize_url(self._http_transport.base)
555
507
        url = urlutils.join(self.base[len('bzr+'):], relpath)
556
508
        url = urlutils.normalize_url(url)
557
 
        return urlutils.relative_url(http_base, url)
 
509
        return urlutils.relative_url(base, url)
 
510
 
 
511
    def abspath(self, relpath):
 
512
        """Return the full url to the given relative path.
 
513
        
 
514
        :param relpath: the relative path or path components
 
515
        :type relpath: str or list
 
516
        """
 
517
        return self._unparse_url(self._combine_paths(self._path, relpath))
558
518
 
559
519
    def clone(self, relative_url):
560
520
        """Make a new RemoteHTTPTransport related to me.
568
528
        smart requests may be different).  This is so that the server doesn't
569
529
        have to handle .bzr/smart requests at arbitrary places inside .bzr
570
530
        directories, just at the initial URL the user uses.
 
531
 
 
532
        The exception is parent paths (i.e. relative_url of "..").
571
533
        """
572
534
        if relative_url:
573
535
            abs_url = self.abspath(relative_url)
574
536
        else:
575
537
            abs_url = self.base
576
 
        return RemoteHTTPTransport(abs_url,
577
 
                                   _from_transport=self,
578
 
                                   http_transport=self._http_transport)
579
 
 
580
 
    def _redirected_to(self, source, target):
581
 
        """See transport._redirected_to"""
582
 
        redirected = self._http_transport._redirected_to(source, target)
583
 
        if (redirected is not None
584
 
            and isinstance(redirected, type(self._http_transport))):
585
 
            return RemoteHTTPTransport('bzr+' + redirected.external_url(),
586
 
                                       http_transport=redirected)
 
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)
587
546
        else:
588
 
            # Either None or a transport for a different protocol
589
 
            return redirected
590
 
 
591
 
 
592
 
class HintingSSHTransport(transport.Transport):
593
 
    """Simple transport that handles ssh:// and points out bzr+ssh://."""
594
 
 
595
 
    def __init__(self, url):
596
 
        raise errors.UnsupportedProtocol(url,
597
 
            'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
 
547
            http_transport = self._http_transport
 
548
        return RemoteHTTPTransport(abs_url, http_transport=http_transport)
598
549
 
599
550
 
600
551
def get_test_permutations():
601
552
    """Return (transport, server) permutations for testing."""
602
553
    ### We may need a little more test framework support to construct an
603
554
    ### appropriate RemoteTransport in the future.
604
 
    from bzrlib.tests import test_server
605
 
    return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]
 
555
    from bzrlib.smart import server
 
556
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]