/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-04-26 01:48:29 UTC
  • mto: This revision was merged to the branch mainline in revision 2457.
  • Revision ID: robertc@robertcollins.net-20070426014829-znbzqzsk1gq68xqh
Fix the 'Unprintable error' message display to use the repr of the
exception that prevented printing the error because the str value for it
is often not useful in debugging (e.g.  KeyError('foo') has a str() of
'foo' but a repr of 'KeyError('foo')' which is much more useful.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
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
 
 
25
from cStringIO import StringIO
 
26
import urllib
 
27
import urlparse
 
28
 
 
29
from bzrlib import (
 
30
    errors,
 
31
    transport,
 
32
    )
 
33
from bzrlib.smart import client, medium, protocol
 
34
 
 
35
# must do this otherwise urllib can't parse the urls properly :(
 
36
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
 
37
    transport.register_urlparse_netloc_protocol(scheme)
 
38
del scheme
 
39
 
 
40
 
 
41
# Port 4155 is the default port for bzr://, registered with IANA.
 
42
BZR_DEFAULT_INTERFACE = '0.0.0.0'
 
43
BZR_DEFAULT_PORT = 4155
 
44
 
 
45
 
 
46
class _SmartStat(object):
 
47
 
 
48
    def __init__(self, size, mode):
 
49
        self.st_size = size
 
50
        self.st_mode = mode
 
51
 
 
52
 
 
53
class RemoteTransport(transport.Transport):
 
54
    """Connection to a smart server.
 
55
 
 
56
    The connection holds references to the medium that can be used to send
 
57
    requests to the server.
 
58
 
 
59
    The connection has a notion of the current directory to which it's
 
60
    connected; this is incorporated in filenames passed to the server.
 
61
    
 
62
    This supports some higher-level RPC operations and can also be treated 
 
63
    like a Transport to do file-like operations.
 
64
 
 
65
    The connection can be made over a tcp socket, an ssh pipe or a series of
 
66
    http requests.  There are concrete subclasses for each type:
 
67
    RemoteTCPTransport, etc.
 
68
    """
 
69
 
 
70
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
 
71
    # responsibilities: Put those on SmartClient or similar. This is vital for
 
72
    # the ability to support multiple versions of the smart protocol over time:
 
73
    # RemoteTransport is an adapter from the Transport object model to the 
 
74
    # SmartClient model, not an encoder.
 
75
 
 
76
    def __init__(self, url, clone_from=None, medium=None):
 
77
        """Constructor.
 
78
 
 
79
        :param medium: The medium to use for this RemoteTransport. This must be
 
80
            supplied if clone_from is None.
 
81
        """
 
82
        ### Technically super() here is faulty because Transport's __init__
 
83
        ### fails to take 2 parameters, and if super were to choose a silly
 
84
        ### initialisation order things would blow up. 
 
85
        if not url.endswith('/'):
 
86
            url += '/'
 
87
        super(RemoteTransport, self).__init__(url)
 
88
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
 
89
                transport.split_url(url)
 
90
        if clone_from is None:
 
91
            self._medium = medium
 
92
        else:
 
93
            # credentials may be stripped from the base in some circumstances
 
94
            # as yet to be clearly defined or documented, so copy them.
 
95
            self._username = clone_from._username
 
96
            # reuse same connection
 
97
            self._medium = clone_from._medium
 
98
        assert self._medium is not None
 
99
 
 
100
    def abspath(self, relpath):
 
101
        """Return the full url to the given relative path.
 
102
        
 
103
        @param relpath: the relative path or path components
 
104
        @type relpath: str or list
 
105
        """
 
106
        return self._unparse_url(self._remote_path(relpath))
 
107
    
 
108
    def clone(self, relative_url):
 
109
        """Make a new RemoteTransport related to me, sharing the same connection.
 
110
 
 
111
        This essentially opens a handle on a different remote directory.
 
112
        """
 
113
        if relative_url is None:
 
114
            return RemoteTransport(self.base, self)
 
115
        else:
 
116
            return RemoteTransport(self.abspath(relative_url), self)
 
117
 
 
118
    def is_readonly(self):
 
119
        """Smart server transport can do read/write file operations."""
 
120
        resp = self._call2('Transport.is_readonly')
 
121
        if resp == ('yes', ):
 
122
            return True
 
123
        elif resp == ('no', ):
 
124
            return False
 
125
        else:
 
126
            self._translate_error(resp)
 
127
        assert False, 'weird response %r' % (resp,)
 
128
 
 
129
    def get_smart_client(self):
 
130
        return self._medium
 
131
 
 
132
    def get_smart_medium(self):
 
133
        return self._medium
 
134
                                                   
 
135
    def _unparse_url(self, path):
 
136
        """Return URL for a path.
 
137
 
 
138
        :see: SFTPUrlHandling._unparse_url
 
139
        """
 
140
        # TODO: Eventually it should be possible to unify this with
 
141
        # SFTPUrlHandling._unparse_url?
 
142
        if path == '':
 
143
            path = '/'
 
144
        path = urllib.quote(path)
 
145
        netloc = urllib.quote(self._host)
 
146
        if self._username is not None:
 
147
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
148
        if self._port is not None:
 
149
            netloc = '%s:%d' % (netloc, self._port)
 
150
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
 
151
 
 
152
    def _remote_path(self, relpath):
 
153
        """Returns the Unicode version of the absolute path for relpath."""
 
154
        return self._combine_paths(self._path, relpath)
 
155
 
 
156
    def _call(self, method, *args):
 
157
        resp = self._call2(method, *args)
 
158
        self._translate_error(resp)
 
159
 
 
160
    def _call2(self, method, *args):
 
161
        """Call a method on the remote server."""
 
162
        return client._SmartClient(self._medium).call(method, *args)
 
163
 
 
164
    def _call_with_body_bytes(self, method, args, body):
 
165
        """Call a method on the remote server with body bytes."""
 
166
        smart_client = client._SmartClient(self._medium)
 
167
        return smart_client.call_with_body_bytes(method, args, body)
 
168
 
 
169
    def has(self, relpath):
 
170
        """Indicate whether a remote file of the given name exists or not.
 
171
 
 
172
        :see: Transport.has()
 
173
        """
 
174
        resp = self._call2('has', self._remote_path(relpath))
 
175
        if resp == ('yes', ):
 
176
            return True
 
177
        elif resp == ('no', ):
 
178
            return False
 
179
        else:
 
180
            self._translate_error(resp)
 
181
 
 
182
    def get(self, relpath):
 
183
        """Return file-like object reading the contents of a remote file.
 
184
        
 
185
        :see: Transport.get_bytes()/get_file()
 
186
        """
 
187
        return StringIO(self.get_bytes(relpath))
 
188
 
 
189
    def get_bytes(self, relpath):
 
190
        remote = self._remote_path(relpath)
 
191
        request = self._medium.get_request()
 
192
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
193
        smart_protocol.call('get', remote)
 
194
        resp = smart_protocol.read_response_tuple(True)
 
195
        if resp != ('ok', ):
 
196
            smart_protocol.cancel_read_body()
 
197
            self._translate_error(resp, relpath)
 
198
        return smart_protocol.read_body_bytes()
 
199
 
 
200
    def _serialise_optional_mode(self, mode):
 
201
        if mode is None:
 
202
            return ''
 
203
        else:
 
204
            return '%d' % mode
 
205
 
 
206
    def mkdir(self, relpath, mode=None):
 
207
        resp = self._call2('mkdir', self._remote_path(relpath),
 
208
            self._serialise_optional_mode(mode))
 
209
        self._translate_error(resp)
 
210
 
 
211
    def put_bytes(self, relpath, upload_contents, mode=None):
 
212
        # FIXME: upload_file is probably not safe for non-ascii characters -
 
213
        # should probably just pass all parameters as length-delimited
 
214
        # strings?
 
215
        if type(upload_contents) is unicode:
 
216
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
217
            # compatible with other transports.
 
218
            raise UnicodeEncodeError(
 
219
                'undefined', upload_contents, 0, 1,
 
220
                'put_bytes must be given bytes, not unicode.')
 
221
        resp = self._call_with_body_bytes('put',
 
222
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
 
223
            upload_contents)
 
224
        self._translate_error(resp)
 
225
 
 
226
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
227
                             create_parent_dir=False,
 
228
                             dir_mode=None):
 
229
        """See Transport.put_bytes_non_atomic."""
 
230
        # FIXME: no encoding in the transport!
 
231
        create_parent_str = 'F'
 
232
        if create_parent_dir:
 
233
            create_parent_str = 'T'
 
234
 
 
235
        resp = self._call_with_body_bytes(
 
236
            'put_non_atomic',
 
237
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
 
238
             create_parent_str, self._serialise_optional_mode(dir_mode)),
 
239
            bytes)
 
240
        self._translate_error(resp)
 
241
 
 
242
    def put_file(self, relpath, upload_file, mode=None):
 
243
        # its not ideal to seek back, but currently put_non_atomic_file depends
 
244
        # on transports not reading before failing - which is a faulty
 
245
        # assumption I think - RBC 20060915
 
246
        pos = upload_file.tell()
 
247
        try:
 
248
            return self.put_bytes(relpath, upload_file.read(), mode)
 
249
        except:
 
250
            upload_file.seek(pos)
 
251
            raise
 
252
 
 
253
    def put_file_non_atomic(self, relpath, f, mode=None,
 
254
                            create_parent_dir=False,
 
255
                            dir_mode=None):
 
256
        return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
 
257
                                         create_parent_dir=create_parent_dir,
 
258
                                         dir_mode=dir_mode)
 
259
 
 
260
    def append_file(self, relpath, from_file, mode=None):
 
261
        return self.append_bytes(relpath, from_file.read(), mode)
 
262
        
 
263
    def append_bytes(self, relpath, bytes, mode=None):
 
264
        resp = self._call_with_body_bytes(
 
265
            'append',
 
266
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
 
267
            bytes)
 
268
        if resp[0] == 'appended':
 
269
            return int(resp[1])
 
270
        self._translate_error(resp)
 
271
 
 
272
    def delete(self, relpath):
 
273
        resp = self._call2('delete', self._remote_path(relpath))
 
274
        self._translate_error(resp)
 
275
 
 
276
    def readv(self, relpath, offsets):
 
277
        if not offsets:
 
278
            return
 
279
 
 
280
        offsets = list(offsets)
 
281
 
 
282
        sorted_offsets = sorted(offsets)
 
283
        # turn the list of offsets into a stack
 
284
        offset_stack = iter(offsets)
 
285
        cur_offset_and_size = offset_stack.next()
 
286
        coalesced = list(self._coalesce_offsets(sorted_offsets,
 
287
                               limit=self._max_readv_combine,
 
288
                               fudge_factor=self._bytes_to_read_before_seek))
 
289
 
 
290
        request = self._medium.get_request()
 
291
        smart_protocol = protocol.SmartClientRequestProtocolOne(request)
 
292
        smart_protocol.call_with_body_readv_array(
 
293
            ('readv', self._remote_path(relpath)),
 
294
            [(c.start, c.length) for c in coalesced])
 
295
        resp = smart_protocol.read_response_tuple(True)
 
296
 
 
297
        if resp[0] != 'readv':
 
298
            # This should raise an exception
 
299
            smart_protocol.cancel_read_body()
 
300
            self._translate_error(resp)
 
301
            return
 
302
 
 
303
        # FIXME: this should know how many bytes are needed, for clarity.
 
304
        data = smart_protocol.read_body_bytes()
 
305
        # Cache the results, but only until they have been fulfilled
 
306
        data_map = {}
 
307
        for c_offset in coalesced:
 
308
            if len(data) < c_offset.length:
 
309
                raise errors.ShortReadvError(relpath, c_offset.start,
 
310
                            c_offset.length, actual=len(data))
 
311
            for suboffset, subsize in c_offset.ranges:
 
312
                key = (c_offset.start+suboffset, subsize)
 
313
                data_map[key] = data[suboffset:suboffset+subsize]
 
314
            data = data[c_offset.length:]
 
315
 
 
316
            # Now that we've read some data, see if we can yield anything back
 
317
            while cur_offset_and_size in data_map:
 
318
                this_data = data_map.pop(cur_offset_and_size)
 
319
                yield cur_offset_and_size[0], this_data
 
320
                cur_offset_and_size = offset_stack.next()
 
321
 
 
322
    def rename(self, rel_from, rel_to):
 
323
        self._call('rename',
 
324
                   self._remote_path(rel_from),
 
325
                   self._remote_path(rel_to))
 
326
 
 
327
    def move(self, rel_from, rel_to):
 
328
        self._call('move',
 
329
                   self._remote_path(rel_from),
 
330
                   self._remote_path(rel_to))
 
331
 
 
332
    def rmdir(self, relpath):
 
333
        resp = self._call('rmdir', self._remote_path(relpath))
 
334
 
 
335
    def _translate_error(self, resp, orig_path=None):
 
336
        """Raise an exception from a response"""
 
337
        if resp is None:
 
338
            what = None
 
339
        else:
 
340
            what = resp[0]
 
341
        if what == 'ok':
 
342
            return
 
343
        elif what == 'NoSuchFile':
 
344
            if orig_path is not None:
 
345
                error_path = orig_path
 
346
            else:
 
347
                error_path = resp[1]
 
348
            raise errors.NoSuchFile(error_path)
 
349
        elif what == 'error':
 
350
            raise errors.SmartProtocolError(unicode(resp[1]))
 
351
        elif what == 'FileExists':
 
352
            raise errors.FileExists(resp[1])
 
353
        elif what == 'DirectoryNotEmpty':
 
354
            raise errors.DirectoryNotEmpty(resp[1])
 
355
        elif what == 'ShortReadvError':
 
356
            raise errors.ShortReadvError(resp[1], int(resp[2]),
 
357
                                         int(resp[3]), int(resp[4]))
 
358
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
 
359
            encoding = str(resp[1]) # encoding must always be a string
 
360
            val = resp[2]
 
361
            start = int(resp[3])
 
362
            end = int(resp[4])
 
363
            reason = str(resp[5]) # reason must always be a string
 
364
            if val.startswith('u:'):
 
365
                val = val[2:].decode('utf-8')
 
366
            elif val.startswith('s:'):
 
367
                val = val[2:].decode('base64')
 
368
            if what == 'UnicodeDecodeError':
 
369
                raise UnicodeDecodeError(encoding, val, start, end, reason)
 
370
            elif what == 'UnicodeEncodeError':
 
371
                raise UnicodeEncodeError(encoding, val, start, end, reason)
 
372
        elif what == "ReadOnlyError":
 
373
            raise errors.TransportNotPossible('readonly transport')
 
374
        else:
 
375
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
 
376
 
 
377
    def disconnect(self):
 
378
        self._medium.disconnect()
 
379
 
 
380
    def delete_tree(self, relpath):
 
381
        raise errors.TransportNotPossible('readonly transport')
 
382
 
 
383
    def stat(self, relpath):
 
384
        resp = self._call2('stat', self._remote_path(relpath))
 
385
        if resp[0] == 'stat':
 
386
            return _SmartStat(int(resp[1]), int(resp[2], 8))
 
387
        else:
 
388
            self._translate_error(resp)
 
389
 
 
390
    ## def lock_read(self, relpath):
 
391
    ##     """Lock the given file for shared (read) access.
 
392
    ##     :return: A lock object, which should be passed to Transport.unlock()
 
393
    ##     """
 
394
    ##     # The old RemoteBranch ignore lock for reading, so we will
 
395
    ##     # continue that tradition and return a bogus lock object.
 
396
    ##     class BogusLock(object):
 
397
    ##         def __init__(self, path):
 
398
    ##             self.path = path
 
399
    ##         def unlock(self):
 
400
    ##             pass
 
401
    ##     return BogusLock(relpath)
 
402
 
 
403
    def listable(self):
 
404
        return True
 
405
 
 
406
    def list_dir(self, relpath):
 
407
        resp = self._call2('list_dir', self._remote_path(relpath))
 
408
        if resp[0] == 'names':
 
409
            return [name.encode('ascii') for name in resp[1:]]
 
410
        else:
 
411
            self._translate_error(resp)
 
412
 
 
413
    def iter_files_recursive(self):
 
414
        resp = self._call2('iter_files_recursive', self._remote_path(''))
 
415
        if resp[0] == 'names':
 
416
            return resp[1:]
 
417
        else:
 
418
            self._translate_error(resp)
 
419
 
 
420
 
 
421
class RemoteTCPTransport(RemoteTransport):
 
422
    """Connection to smart server over plain tcp.
 
423
    
 
424
    This is essentially just a factory to get 'RemoteTransport(url,
 
425
        SmartTCPClientMedium).
 
426
    """
 
427
 
 
428
    def __init__(self, url):
 
429
        _scheme, _username, _password, _host, _port, _path = \
 
430
            transport.split_url(url)
 
431
        if _port is None:
 
432
            _port = BZR_DEFAULT_PORT
 
433
        else:
 
434
            try:
 
435
                _port = int(_port)
 
436
            except (ValueError, TypeError), e:
 
437
                raise errors.InvalidURL(
 
438
                    path=url, extra="invalid port %s" % _port)
 
439
        client_medium = medium.SmartTCPClientMedium(_host, _port)
 
440
        super(RemoteTCPTransport, self).__init__(url, medium=client_medium)
 
441
 
 
442
 
 
443
class RemoteSSHTransport(RemoteTransport):
 
444
    """Connection to smart server over SSH.
 
445
 
 
446
    This is essentially just a factory to get 'RemoteTransport(url,
 
447
        SmartSSHClientMedium).
 
448
    """
 
449
 
 
450
    def __init__(self, url):
 
451
        _scheme, _username, _password, _host, _port, _path = \
 
452
            transport.split_url(url)
 
453
        try:
 
454
            if _port is not None:
 
455
                _port = int(_port)
 
456
        except (ValueError, TypeError), e:
 
457
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
 
458
                _port)
 
459
        client_medium = medium.SmartSSHClientMedium(_host, _port,
 
460
                                                    _username, _password)
 
461
        super(RemoteSSHTransport, self).__init__(url, medium=client_medium)
 
462
 
 
463
 
 
464
class RemoteHTTPTransport(RemoteTransport):
 
465
    """Just a way to connect between a bzr+http:// url and http://.
 
466
    
 
467
    This connection operates slightly differently than the RemoteSSHTransport.
 
468
    It uses a plain http:// transport underneath, which defines what remote
 
469
    .bzr/smart URL we are connected to. From there, all paths that are sent are
 
470
    sent as relative paths, this way, the remote side can properly
 
471
    de-reference them, since it is likely doing rewrite rules to translate an
 
472
    HTTP path into a local path.
 
473
    """
 
474
 
 
475
    def __init__(self, url, http_transport=None):
 
476
        assert url.startswith('bzr+http://')
 
477
 
 
478
        if http_transport is None:
 
479
            http_url = url[len('bzr+'):]
 
480
            self._http_transport = transport.get_transport(http_url)
 
481
        else:
 
482
            self._http_transport = http_transport
 
483
        http_medium = self._http_transport.get_smart_medium()
 
484
        super(RemoteHTTPTransport, self).__init__(url, medium=http_medium)
 
485
 
 
486
    def _remote_path(self, relpath):
 
487
        """After connecting HTTP Transport only deals in relative URLs."""
 
488
        if relpath == '.':
 
489
            return ''
 
490
        else:
 
491
            return relpath
 
492
 
 
493
    def abspath(self, relpath):
 
494
        """Return the full url to the given relative path.
 
495
        
 
496
        :param relpath: the relative path or path components
 
497
        :type relpath: str or list
 
498
        """
 
499
        return self._unparse_url(self._combine_paths(self._path, relpath))
 
500
 
 
501
    def clone(self, relative_url):
 
502
        """Make a new RemoteHTTPTransport related to me.
 
503
 
 
504
        This is re-implemented rather than using the default
 
505
        RemoteTransport.clone() because we must be careful about the underlying
 
506
        http transport.
 
507
        """
 
508
        if relative_url:
 
509
            abs_url = self.abspath(relative_url)
 
510
        else:
 
511
            abs_url = self.base
 
512
        # By cloning the underlying http_transport, we are able to share the
 
513
        # connection.
 
514
        new_transport = self._http_transport.clone(relative_url)
 
515
        return RemoteHTTPTransport(abs_url, http_transport=new_transport)
 
516
 
 
517
 
 
518
def get_test_permutations():
 
519
    """Return (transport, server) permutations for testing."""
 
520
    ### We may need a little more test framework support to construct an
 
521
    ### appropriate RemoteTransport in the future.
 
522
    from bzrlib.smart import server
 
523
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]