/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: Andrew Bennetts
  • Date: 2008-03-28 08:05:51 UTC
  • mto: This revision was merged to the branch mainline in revision 3321.
  • Revision ID: andrew.bennetts@canonical.com-20080328080551-n7f6rejuycnzn0p8
Change _SmartClient's API to accept a medium and a base, rather than a _SharedConnection.

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