~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-04-28 15:04:17 UTC
  • mfrom: (2466 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2566.
  • Revision ID: john@arbash-meinel.com-20070428150417-trp3pi0pzd411pu4
[merge] bzr.dev 2466

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
77
    def __init__(self, url, clone_from=None, medium=None):
76
85
        ### initialisation order things would blow up. 
77
86
        if not url.endswith('/'):
78
87
            url += '/'
79
 
        super(SmartTransport, self).__init__(url)
 
88
        super(RemoteTransport, self).__init__(url)
80
89
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
81
90
                transport.split_url(url)
82
91
        if clone_from is None:
98
107
        return self._unparse_url(self._remote_path(relpath))
99
108
    
100
109
    def clone(self, relative_url):
101
 
        """Make a new SmartTransport related to me, sharing the same connection.
 
110
        """Make a new RemoteTransport related to me, sharing the same connection.
102
111
 
103
112
        This essentially opens a handle on a different remote directory.
104
113
        """
105
114
        if relative_url is None:
106
 
            return SmartTransport(self.base, self)
 
115
            return RemoteTransport(self.base, self)
107
116
        else:
108
 
            return SmartTransport(self.abspath(relative_url), self)
 
117
            return RemoteTransport(self.abspath(relative_url), self)
109
118
 
110
119
    def is_readonly(self):
111
120
        """Smart server transport can do read/write file operations."""
112
 
        return False
113
 
                                                   
 
121
        resp = self._call2('Transport.is_readonly')
 
122
        if resp == ('yes', ):
 
123
            return True
 
124
        elif resp == ('no', ):
 
125
            return False
 
126
        else:
 
127
            self._translate_error(resp)
 
128
        assert False, 'weird response %r' % (resp,)
 
129
 
114
130
    def get_smart_client(self):
115
131
        return self._medium
116
132
 
144
160
 
145
161
    def _call2(self, method, *args):
146
162
        """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()
 
163
        return client._SmartClient(self._medium).call(method, *args)
150
164
 
151
165
    def _call_with_body_bytes(self, method, args, body):
152
166
        """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()
 
167
        smart_client = client._SmartClient(self._medium)
 
168
        return smart_client.call_with_body_bytes(method, args, body)
156
169
 
157
170
    def has(self, relpath):
158
171
        """Indicate whether a remote file of the given name exists or not.
176
189
 
177
190
    def get_bytes(self, relpath):
178
191
        remote = self._remote_path(relpath)
179
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
180
 
        protocol.call('get', remote)
181
 
        resp = protocol.read_response_tuple(True)
 
192
        request = self._medium.get_request()
 
193
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
194
        smart_protocol.call('get', remote)
 
195
        resp = smart_protocol.read_response_tuple(True)
182
196
        if resp != ('ok', ):
183
 
            protocol.cancel_read_body()
 
197
            smart_protocol.cancel_read_body()
184
198
            self._translate_error(resp, relpath)
185
 
        return protocol.read_body_bytes()
 
199
        return smart_protocol.read_body_bytes()
186
200
 
187
201
    def _serialise_optional_mode(self, mode):
188
202
        if mode is None:
199
213
        # FIXME: upload_file is probably not safe for non-ascii characters -
200
214
        # should probably just pass all parameters as length-delimited
201
215
        # strings?
 
216
        if type(upload_contents) is unicode:
 
217
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
218
            # compatible with other transports.
 
219
            raise UnicodeEncodeError(
 
220
                'undefined', upload_contents, 0, 1,
 
221
                'put_bytes must be given bytes, not unicode.')
202
222
        resp = self._call_with_body_bytes('put',
203
223
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
204
224
            upload_contents)
268
288
                               limit=self._max_readv_combine,
269
289
                               fudge_factor=self._bytes_to_read_before_seek))
270
290
 
271
 
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
272
 
        protocol.call_with_body_readv_array(
 
291
        request = self._medium.get_request()
 
292
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
293
        smart_protocol.call_with_body_readv_array(
273
294
            ('readv', self._remote_path(relpath)),
274
295
            [(c.start, c.length) for c in coalesced])
275
 
        resp = protocol.read_response_tuple(True)
 
296
        resp = smart_protocol.read_response_tuple(True)
276
297
 
277
298
        if resp[0] != 'readv':
278
299
            # This should raise an exception
279
 
            protocol.cancel_read_body()
 
300
            smart_protocol.cancel_read_body()
280
301
            self._translate_error(resp)
281
302
            return
282
303
 
283
304
        # FIXME: this should know how many bytes are needed, for clarity.
284
 
        data = protocol.read_body_bytes()
 
305
        data = smart_protocol.read_body_bytes()
285
306
        # Cache the results, but only until they have been fulfilled
286
307
        data_map = {}
287
308
        for c_offset in coalesced:
363
384
    def stat(self, relpath):
364
385
        resp = self._call2('stat', self._remote_path(relpath))
365
386
        if resp[0] == 'stat':
366
 
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
387
            return _SmartStat(int(resp[1]), int(resp[2], 8))
367
388
        else:
368
389
            self._translate_error(resp)
369
390
 
398
419
            self._translate_error(resp)
399
420
 
400
421
 
401
 
 
402
 
class SmartTCPTransport(SmartTransport):
 
422
class RemoteTCPTransport(RemoteTransport):
403
423
    """Connection to smart server over plain tcp.
404
424
    
405
425
    This is essentially just a factory to get 'RemoteTransport(url,
417
437
            except (ValueError, TypeError), e:
418
438
                raise errors.InvalidURL(
419
439
                    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):
 
440
        client_medium = medium.SmartTCPClientMedium(_host, _port)
 
441
        super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
 
442
 
 
443
 
 
444
class RemoteSSHTransport(RemoteTransport):
425
445
    """Connection to smart server over SSH.
426
446
 
427
447
    This is essentially just a factory to get 'RemoteTransport(url,
437
457
        except (ValueError, TypeError), e:
438
458
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
439
459
                _port)
440
 
        medium = SmartSSHClientMedium(_host, _port, _username, _password)
441
 
        super(SmartSSHTransport, self).__init__(url, medium=medium)
442
 
 
443
 
 
444
 
class SmartHTTPTransport(SmartTransport):
 
460
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
461
                                                    _username, _password)
 
462
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
 
463
 
 
464
 
 
465
class RemoteHTTPTransport(RemoteTransport):
445
466
    """Just a way to connect between a bzr+http:// url and http://.
446
467
    
447
 
    This connection operates slightly differently than the SmartSSHTransport.
 
468
    This connection operates slightly differently than the RemoteSSHTransport.
448
469
    It uses a plain http:// transport underneath, which defines what remote
449
470
    .bzr/smart URL we are connected to. From there, all paths that are sent are
450
471
    sent as relative paths, this way, the remote side can properly
461
482
        else:
462
483
            self._http_transport = http_transport
463
484
        http_medium = self._http_transport.get_smart_medium()
464
 
        super(SmartHTTPTransport, self).__init__(url, medium=http_medium)
 
485
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
465
486
 
466
487
    def _remote_path(self, relpath):
467
488
        """After connecting HTTP Transport only deals in relative URLs."""
468
 
        if relpath == '.':
469
 
            return ''
470
 
        else:
471
 
            return relpath
 
489
        # Adjust the relpath based on which URL this smart transport is
 
490
        # connected to.
 
491
        base = self._http_transport.base
 
492
        url = urlutils.join(self.base[len('bzr+'):], relpath)
 
493
        url = urlutils.normalize_url(url)
 
494
        return urlutils.relative_url(base, url)
472
495
 
473
496
    def abspath(self, relpath):
474
497
        """Return the full url to the given relative path.
479
502
        return self._unparse_url(self._combine_paths(self._path, relpath))
480
503
 
481
504
    def clone(self, relative_url):
482
 
        """Make a new SmartHTTPTransport related to me.
 
505
        """Make a new RemoteHTTPTransport related to me.
483
506
 
484
507
        This is re-implemented rather than using the default
485
 
        SmartTransport.clone() because we must be careful about the underlying
 
508
        RemoteTransport.clone() because we must be careful about the underlying
486
509
        http transport.
 
510
 
 
511
        Also, the cloned smart transport will POST to the same .bzr/smart
 
512
        location as this transport (although obviously the relative paths in the
 
513
        smart requests may be different).  This is so that the server doesn't
 
514
        have to handle .bzr/smart requests at arbitrary places inside .bzr
 
515
        directories, just at the initial URL the user uses.
 
516
 
 
517
        The exception is parent paths (i.e. relative_url of "..").
487
518
        """
488
519
        if relative_url:
489
520
            abs_url = self.abspath(relative_url)
490
521
        else:
491
522
            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)
 
523
        # We either use the exact same http_transport (for child locations), or
 
524
        # a clone of the underlying http_transport (for parent locations).  This
 
525
        # means we share the connection.
 
526
        normalized_rel_url = urlutils.relative_url(self.base, abs_url)
 
527
        if normalized_rel_url == ".." or normalized_rel_url.startswith("../"):
 
528
            http_transport = self._http_transport.clone(normalized_rel_url)
 
529
        else:
 
530
            http_transport = self._http_transport
 
531
        return RemoteHTTPTransport(abs_url, http_transport=http_transport)
496
532
 
497
533
 
498
534
def get_test_permutations():
499
535
    """Return (transport, server) permutations for testing."""
500
 
    from bzrlib.smart import server
501
536
    ### We may need a little more test framework support to construct an
502
537
    ### appropriate RemoteTransport in the future.
503
 
    return [(SmartTCPTransport, server.SmartTCPServer_for_testing)]
 
538
    from bzrlib.smart import server
 
539
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]