/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: Canonical.com Patch Queue Manager
  • Date: 2007-04-24 14:19:24 UTC
  • mfrom: (2449.1.1 trivial)
  • Revision ID: pqm@pqm.ubuntu.com-20070424141924-i4w482pi1pb95pob
(bialix) fix RSTX wrong formatting in HACKING (trivial)

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)]