/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: 2008-04-09 20:13:46 UTC
  • mto: This revision was merged to the branch mainline in revision 3350.
  • Revision ID: robertc@robertcollins.net-20080409201346-2m5dpim3cowzwacs
Spelling in NEWS.

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