~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: John Arbash Meinel
  • Date: 2007-07-15 14:33:30 UTC
  • mto: This revision was merged to the branch mainline in revision 2620.
  • Revision ID: john@arbash-meinel.com-20070715143330-6ddg2ay25b4pxiu1
Try another form of comment

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
"""RemoteTransport client for the smart-server.
 
18
 
 
19
This module shouldn't be accessed directly.  The classes defined here should be
 
20
imported from bzrlib.smart.
 
21
"""
 
22
 
 
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
 
24
 
17
25
from cStringIO import StringIO
18
26
import urllib
19
27
import urlparse
21
29
from bzrlib import (
22
30
    errors,
23
31
    transport,
 
32
    urlutils,
24
33
    )
25
 
from bzrlib.smart.protocol import SmartClientRequestProtocolOne
26
 
from bzrlib.smart.medium import SmartTCPClientMedium, SmartSSHClientMedium
 
34
from bzrlib.smart import client, medium, protocol
27
35
 
28
36
# must do this otherwise urllib can't parse the urls properly :(
29
37
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
32
40
 
33
41
 
34
42
# Port 4155 is the default port for bzr://, registered with IANA.
 
43
BZR_DEFAULT_INTERFACE = '0.0.0.0'
35
44
BZR_DEFAULT_PORT = 4155
36
45
 
37
46
 
38
 
class SmartStat(object):
 
47
class _SmartStat(object):
39
48
 
40
49
    def __init__(self, size, mode):
41
50
        self.st_size = size
42
51
        self.st_mode = mode
43
52
 
44
53
 
45
 
class SmartTransport(transport.Transport):
 
54
class RemoteTransport(transport.Transport):
46
55
    """Connection to a smart server.
47
56
 
48
 
    The connection holds references to pipes that can be used to send requests
49
 
    to the server.
 
57
    The connection holds references to the medium that can be used to send
 
58
    requests to the server.
50
59
 
51
60
    The connection has a notion of the current directory to which it's
52
61
    connected; this is incorporated in filenames passed to the server.
54
63
    This supports some higher-level RPC operations and can also be treated 
55
64
    like a Transport to do file-like operations.
56
65
 
57
 
    The connection can be made over a tcp socket, or (in future) an ssh pipe
58
 
    or a series of http requests.  There are concrete subclasses for each
59
 
    type: SmartTCPTransport, etc.
 
66
    The connection can be made over a tcp socket, an ssh pipe or a series of
 
67
    http requests.  There are concrete subclasses for each type:
 
68
    RemoteTCPTransport, etc.
60
69
    """
61
70
 
62
 
    # IMPORTANT FOR IMPLEMENTORS: SmartTransport MUST NOT be given encoding
 
71
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
63
72
    # responsibilities: Put those on SmartClient or similar. This is vital for
64
73
    # the ability to support multiple versions of the smart protocol over time:
65
 
    # SmartTransport is an adapter from the Transport object model to the 
 
74
    # RemoteTransport is an adapter from the Transport object model to the 
66
75
    # SmartClient model, not an encoder.
67
76
 
68
 
    def __init__(self, url, clone_from=None, medium=None):
 
77
    def __init__(self, url, clone_from=None, medium=None, _client=None):
69
78
        """Constructor.
70
79
 
 
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.
71
83
        :param medium: The medium to use for this RemoteTransport. This must be
72
84
            supplied if clone_from is None.
 
85
        :param _client: Override the _SmartClient used by this transport.  This
 
86
            should only be used for testing purposes; normally this is
 
87
            determined from the medium.
73
88
        """
74
89
        ### Technically super() here is faulty because Transport's __init__
75
90
        ### fails to take 2 parameters, and if super were to choose a silly
76
91
        ### initialisation order things would blow up. 
77
92
        if not url.endswith('/'):
78
93
            url += '/'
79
 
        super(SmartTransport, self).__init__(url)
 
94
        super(RemoteTransport, self).__init__(url)
80
95
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
81
96
                transport.split_url(url)
82
97
        if clone_from is None:
88
103
            # reuse same connection
89
104
            self._medium = clone_from._medium
90
105
        assert self._medium is not None
 
106
        if _client is None:
 
107
            self._client = client._SmartClient(self._medium)
 
108
        else:
 
109
            self._client = _client
91
110
 
92
111
    def abspath(self, relpath):
93
112
        """Return the full url to the given relative path.
98
117
        return self._unparse_url(self._remote_path(relpath))
99
118
    
100
119
    def clone(self, relative_url):
101
 
        """Make a new SmartTransport related to me, sharing the same connection.
 
120
        """Make a new RemoteTransport related to me, sharing the same connection.
102
121
 
103
122
        This essentially opens a handle on a different remote directory.
104
123
        """
105
124
        if relative_url is None:
106
 
            return SmartTransport(self.base, self)
 
125
            return RemoteTransport(self.base, self)
107
126
        else:
108
 
            return SmartTransport(self.abspath(relative_url), self)
 
127
            return RemoteTransport(self.abspath(relative_url), self)
109
128
 
110
129
    def is_readonly(self):
111
130
        """Smart server transport can do read/write file operations."""
112
 
        return False
113
 
                                                   
 
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'") or
 
138
              resp == ('error', "Generic bzr smart protocol error: "
 
139
                                "bad request u'Transport.is_readonly'")):
 
140
            # XXX: nasty hack: servers before 0.16 don't have a
 
141
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
 
142
            # did: assume False.
 
143
            return False
 
144
        else:
 
145
            self._translate_error(resp)
 
146
        raise errors.UnexpectedSmartServerResponse(resp)
 
147
 
114
148
    def get_smart_client(self):
115
149
        return self._medium
116
150
 
144
178
 
145
179
    def _call2(self, method, *args):
146
180
        """Call a method on the remote server."""
147
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
148
 
        protocol.call(method, *args)
149
 
        return protocol.read_response_tuple()
 
181
        return self._client.call(method, *args)
150
182
 
151
183
    def _call_with_body_bytes(self, method, args, body):
152
184
        """Call a method on the remote server with body bytes."""
153
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
154
 
        protocol.call_with_body_bytes((method, ) + args, body)
155
 
        return protocol.read_response_tuple()
 
185
        return self._client.call_with_body_bytes(method, args, body)
156
186
 
157
187
    def has(self, relpath):
158
188
        """Indicate whether a remote file of the given name exists or not.
176
206
 
177
207
    def get_bytes(self, relpath):
178
208
        remote = self._remote_path(relpath)
179
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
180
 
        protocol.call('get', remote)
181
 
        resp = protocol.read_response_tuple(True)
 
209
        request = self._medium.get_request()
 
210
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
211
        smart_protocol.call('get', remote)
 
212
        resp = smart_protocol.read_response_tuple(True)
182
213
        if resp != ('ok', ):
183
 
            protocol.cancel_read_body()
 
214
            smart_protocol.cancel_read_body()
184
215
            self._translate_error(resp, relpath)
185
 
        return protocol.read_body_bytes()
 
216
        return smart_protocol.read_body_bytes()
186
217
 
187
218
    def _serialise_optional_mode(self, mode):
188
219
        if mode is None:
199
230
        # FIXME: upload_file is probably not safe for non-ascii characters -
200
231
        # should probably just pass all parameters as length-delimited
201
232
        # strings?
 
233
        if type(upload_contents) is unicode:
 
234
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
235
            # compatible with other transports.
 
236
            raise UnicodeEncodeError(
 
237
                'undefined', upload_contents, 0, 1,
 
238
                'put_bytes must be given bytes, not unicode.')
202
239
        resp = self._call_with_body_bytes('put',
203
240
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
204
241
            upload_contents)
268
305
                               limit=self._max_readv_combine,
269
306
                               fudge_factor=self._bytes_to_read_before_seek))
270
307
 
271
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
272
 
        protocol.call_with_body_readv_array(
 
308
        request = self._medium.get_request()
 
309
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
310
        smart_protocol.call_with_body_readv_array(
273
311
            ('readv', self._remote_path(relpath)),
274
312
            [(c.start, c.length) for c in coalesced])
275
 
        resp = protocol.read_response_tuple(True)
 
313
        resp = smart_protocol.read_response_tuple(True)
276
314
 
277
315
        if resp[0] != 'readv':
278
316
            # This should raise an exception
279
 
            protocol.cancel_read_body()
 
317
            smart_protocol.cancel_read_body()
280
318
            self._translate_error(resp)
281
319
            return
282
320
 
283
321
        # FIXME: this should know how many bytes are needed, for clarity.
284
 
        data = protocol.read_body_bytes()
 
322
        data = smart_protocol.read_body_bytes()
285
323
        # Cache the results, but only until they have been fulfilled
286
324
        data_map = {}
287
325
        for c_offset in coalesced:
351
389
                raise UnicodeEncodeError(encoding, val, start, end, reason)
352
390
        elif what == "ReadOnlyError":
353
391
            raise errors.TransportNotPossible('readonly transport')
 
392
        elif what == "ReadError":
 
393
            if orig_path is not None:
 
394
                error_path = orig_path
 
395
            else:
 
396
                error_path = resp[1]
 
397
            raise errors.ReadError(error_path)
354
398
        else:
355
399
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
356
400
 
363
407
    def stat(self, relpath):
364
408
        resp = self._call2('stat', self._remote_path(relpath))
365
409
        if resp[0] == 'stat':
366
 
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
410
            return _SmartStat(int(resp[1]), int(resp[2], 8))
367
411
        else:
368
412
            self._translate_error(resp)
369
413
 
398
442
            self._translate_error(resp)
399
443
 
400
444
 
401
 
 
402
 
class SmartTCPTransport(SmartTransport):
 
445
class RemoteTCPTransport(RemoteTransport):
403
446
    """Connection to smart server over plain tcp.
404
447
    
405
448
    This is essentially just a factory to get 'RemoteTransport(url,
417
460
            except (ValueError, TypeError), e:
418
461
                raise errors.InvalidURL(
419
462
                    path=url, extra="invalid port %s" % _port)
420
 
        medium = SmartTCPClientMedium(_host, _port)
421
 
        super(SmartTCPTransport, self).__init__(url, medium=medium)
422
 
 
423
 
 
424
 
class SmartSSHTransport(SmartTransport):
 
463
        client_medium = medium.SmartTCPClientMedium(_host, _port)
 
464
        super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
 
465
 
 
466
 
 
467
class RemoteSSHTransport(RemoteTransport):
425
468
    """Connection to smart server over SSH.
426
469
 
427
470
    This is essentially just a factory to get 'RemoteTransport(url,
437
480
        except (ValueError, TypeError), e:
438
481
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
439
482
                _port)
440
 
        medium = SmartSSHClientMedium(_host, _port, _username, _password)
441
 
        super(SmartSSHTransport, self).__init__(url, medium=medium)
442
 
 
443
 
 
444
 
class SmartHTTPTransport(SmartTransport):
 
483
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
484
                                                    _username, _password)
 
485
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
 
486
 
 
487
 
 
488
class RemoteHTTPTransport(RemoteTransport):
445
489
    """Just a way to connect between a bzr+http:// url and http://.
446
490
    
447
 
    This connection operates slightly differently than the SmartSSHTransport.
 
491
    This connection operates slightly differently than the RemoteSSHTransport.
448
492
    It uses a plain http:// transport underneath, which defines what remote
449
493
    .bzr/smart URL we are connected to. From there, all paths that are sent are
450
494
    sent as relative paths, this way, the remote side can properly
461
505
        else:
462
506
            self._http_transport = http_transport
463
507
        http_medium = self._http_transport.get_smart_medium()
464
 
        super(SmartHTTPTransport, self).__init__(url, medium=http_medium)
 
508
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
465
509
 
466
510
    def _remote_path(self, relpath):
467
511
        """After connecting HTTP Transport only deals in relative URLs."""
468
 
        if relpath == '.':
469
 
            return ''
470
 
        else:
471
 
            return relpath
 
512
        # Adjust the relpath based on which URL this smart transport is
 
513
        # connected to.
 
514
        base = urlutils.normalize_url(self._http_transport.base)
 
515
        url = urlutils.join(self.base[len('bzr+'):], relpath)
 
516
        url = urlutils.normalize_url(url)
 
517
        return urlutils.relative_url(base, url)
472
518
 
473
519
    def abspath(self, relpath):
474
520
        """Return the full url to the given relative path.
479
525
        return self._unparse_url(self._combine_paths(self._path, relpath))
480
526
 
481
527
    def clone(self, relative_url):
482
 
        """Make a new SmartHTTPTransport related to me.
 
528
        """Make a new RemoteHTTPTransport related to me.
483
529
 
484
530
        This is re-implemented rather than using the default
485
 
        SmartTransport.clone() because we must be careful about the underlying
 
531
        RemoteTransport.clone() because we must be careful about the underlying
486
532
        http transport.
 
533
 
 
534
        Also, the cloned smart transport will POST to the same .bzr/smart
 
535
        location as this transport (although obviously the relative paths in the
 
536
        smart requests may be different).  This is so that the server doesn't
 
537
        have to handle .bzr/smart requests at arbitrary places inside .bzr
 
538
        directories, just at the initial URL the user uses.
 
539
 
 
540
        The exception is parent paths (i.e. relative_url of "..").
487
541
        """
488
542
        if relative_url:
489
543
            abs_url = self.abspath(relative_url)
490
544
        else:
491
545
            abs_url = self.base
492
 
        # By cloning the underlying http_transport, we are able to share the
493
 
        # connection.
494
 
        new_transport = self._http_transport.clone(relative_url)
495
 
        return SmartHTTPTransport(abs_url, http_transport=new_transport)
 
546
        # We either use the exact same http_transport (for child locations), or
 
547
        # a clone of the underlying http_transport (for parent locations).  This
 
548
        # means we share the connection.
 
549
        norm_base = urlutils.normalize_url(self.base)
 
550
        norm_abs_url = urlutils.normalize_url(abs_url)
 
551
        normalized_rel_url = urlutils.relative_url(norm_base, norm_abs_url)
 
552
        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
 
553
            http_transport = self._http_transport.clone(normalized_rel_url)
 
554
        else:
 
555
            http_transport = self._http_transport
 
556
        return RemoteHTTPTransport(abs_url, http_transport=http_transport)
496
557
 
497
558
 
498
559
def get_test_permutations():
499
560
    """Return (transport, server) permutations for testing."""
500
 
    from bzrlib.smart import server
501
561
    ### We may need a little more test framework support to construct an
502
562
    ### appropriate RemoteTransport in the future.
503
 
    return [(SmartTCPTransport, server.SmartTCPServer_for_testing)]
 
563
    from bzrlib.smart import server
 
564
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]