~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-09-11 06:10:59 UTC
  • mfrom: (3702.1.1 trivial)
  • Revision ID: pqm@pqm.ubuntu.com-20080911061059-svzqfejar17ui4zw
(mbp) KnitVersionedFiles repr

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
 
1
# Copyright (C) 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""RemoteTransport client for the smart-server.
18
18
 
28
28
    config,
29
29
    debug,
30
30
    errors,
31
 
    remote,
32
31
    trace,
33
32
    transport,
34
33
    urlutils,
35
34
    )
36
35
from bzrlib.smart import client, medium
37
 
from bzrlib.symbol_versioning import (
38
 
    deprecated_method,
39
 
    )
 
36
from bzrlib.symbol_versioning import (deprecated_method, one_four)
40
37
 
41
38
 
42
39
class _SmartStat(object):
46
43
        self.st_mode = mode
47
44
 
48
45
 
49
 
class RemoteTransport(transport.ConnectedTransport):
 
46
class RemoteTransport(transport.ConnectedTransport, medium.SmartClientMedium):
50
47
    """Connection to a smart server.
51
48
 
52
49
    The connection holds references to the medium that can be used to send
54
51
 
55
52
    The connection has a notion of the current directory to which it's
56
53
    connected; this is incorporated in filenames passed to the server.
57
 
 
58
 
    This supports some higher-level RPC operations and can also be treated
 
54
    
 
55
    This supports some higher-level RPC operations and can also be treated 
59
56
    like a Transport to do file-like operations.
60
57
 
61
58
    The connection can be made over a tcp socket, an ssh pipe or a series of
63
60
    RemoteTCPTransport, etc.
64
61
    """
65
62
 
66
 
    # When making a readv request, cap it at requesting 5MB of data
67
 
    _max_readv_bytes = 5*1024*1024
68
 
 
69
63
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
70
64
    # responsibilities: Put those on SmartClient or similar. This is vital for
71
65
    # the ability to support multiple versions of the smart protocol over time:
72
 
    # RemoteTransport is an adapter from the Transport object model to the
 
66
    # RemoteTransport is an adapter from the Transport object model to the 
73
67
    # SmartClient model, not an encoder.
74
68
 
75
69
    # FIXME: the medium parameter should be private, only the tests requires
92
86
            should only be used for testing purposes; normally this is
93
87
            determined from the medium.
94
88
        """
95
 
        super(RemoteTransport, self).__init__(
96
 
            url, _from_transport=_from_transport)
 
89
        super(RemoteTransport, self).__init__(url,
 
90
                                              _from_transport=_from_transport)
97
91
 
98
92
        # The medium is the connection, except when we need to share it with
99
93
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
94
        # what we want to share is really the shared connection.
101
95
 
102
 
        if (_from_transport is not None
103
 
            and isinstance(_from_transport, RemoteTransport)):
104
 
            _client = _from_transport._client
105
 
        elif _from_transport is None:
 
96
        if _from_transport is None:
106
97
            # If no _from_transport is specified, we need to intialize the
107
98
            # shared medium.
108
99
            credentials = None
138
129
        # No credentials
139
130
        return None, None
140
131
 
141
 
    def _report_activity(self, bytes, direction):
142
 
        """See Transport._report_activity.
143
 
 
144
 
        Does nothing; the smart medium will report activity triggered by a
145
 
        RemoteTransport.
146
 
        """
147
 
        pass
148
 
 
149
132
    def is_readonly(self):
150
133
        """Smart server transport can do read/write file operations."""
151
134
        try:
160
143
        elif resp == ('no', ):
161
144
            return False
162
145
        else:
163
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
146
            self._translate_error(resp)
 
147
        raise errors.UnexpectedSmartServerResponse(resp)
164
148
 
165
149
    def get_smart_client(self):
166
150
        return self._get_connection()
168
152
    def get_smart_medium(self):
169
153
        return self._get_connection()
170
154
 
 
155
    @deprecated_method(one_four)
 
156
    def get_shared_medium(self):
 
157
        return self._get_shared_connection()
 
158
 
171
159
    def _remote_path(self, relpath):
172
160
        """Returns the Unicode version of the absolute path for relpath."""
173
 
        return urlutils.URL._combine_paths(self._parsed_url.path, relpath)
 
161
        return self._combine_paths(self._path, relpath)
174
162
 
175
163
    def _call(self, method, *args):
176
 
        resp = self._call2(method, *args)
177
 
        self._ensure_ok(resp)
 
164
        try:
 
165
            resp = self._call2(method, *args)
 
166
        except errors.ErrorFromSmartServer, err:
 
167
            self._translate_error(err.error_tuple)
 
168
        self._translate_error(resp)
178
169
 
179
170
    def _call2(self, method, *args):
180
171
        """Call a method on the remote server."""
181
172
        try:
182
173
            return self._client.call(method, *args)
183
174
        except errors.ErrorFromSmartServer, err:
184
 
            # The first argument, if present, is always a path.
185
 
            if args:
186
 
                context = {'relpath': args[0]}
187
 
            else:
188
 
                context = {}
189
 
            self._translate_error(err, **context)
 
175
            self._translate_error(err.error_tuple)
190
176
 
191
177
    def _call_with_body_bytes(self, method, args, body):
192
178
        """Call a method on the remote server with body bytes."""
193
179
        try:
194
180
            return self._client.call_with_body_bytes(method, args, body)
195
181
        except errors.ErrorFromSmartServer, err:
196
 
            # The first argument, if present, is always a path.
197
 
            if args:
198
 
                context = {'relpath': args[0]}
199
 
            else:
200
 
                context = {}
201
 
            self._translate_error(err, **context)
 
182
            self._translate_error(err.error_tuple)
202
183
 
203
184
    def has(self, relpath):
204
185
        """Indicate whether a remote file of the given name exists or not.
211
192
        elif resp == ('no', ):
212
193
            return False
213
194
        else:
214
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
195
            self._translate_error(resp)
215
196
 
216
197
    def get(self, relpath):
217
198
        """Return file-like object reading the contents of a remote file.
218
 
 
 
199
        
219
200
        :see: Transport.get_bytes()/get_file()
220
201
        """
221
202
        return StringIO(self.get_bytes(relpath))
225
206
        try:
226
207
            resp, response_handler = self._client.call_expecting_body('get', remote)
227
208
        except errors.ErrorFromSmartServer, err:
228
 
            self._translate_error(err, relpath)
 
209
            self._translate_error(err.error_tuple, relpath)
229
210
        if resp != ('ok', ):
230
211
            response_handler.cancel_read_body()
231
212
            raise errors.UnexpectedSmartServerResponse(resp)
240
221
    def mkdir(self, relpath, mode=None):
241
222
        resp = self._call2('mkdir', self._remote_path(relpath),
242
223
            self._serialise_optional_mode(mode))
 
224
        self._translate_error(resp)
243
225
 
244
226
    def open_write_stream(self, relpath, mode=None):
245
227
        """See Transport.open_write_stream."""
261
243
        resp = self._call_with_body_bytes('put',
262
244
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
263
245
            upload_contents)
264
 
        self._ensure_ok(resp)
 
246
        self._translate_error(resp)
265
247
        return len(upload_contents)
266
248
 
267
249
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
278
260
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
279
261
             create_parent_str, self._serialise_optional_mode(dir_mode)),
280
262
            bytes)
281
 
        self._ensure_ok(resp)
 
263
        self._translate_error(resp)
282
264
 
283
265
    def put_file(self, relpath, upload_file, mode=None):
284
266
        # its not ideal to seek back, but currently put_non_atomic_file depends
300
282
 
301
283
    def append_file(self, relpath, from_file, mode=None):
302
284
        return self.append_bytes(relpath, from_file.read(), mode)
303
 
 
 
285
        
304
286
    def append_bytes(self, relpath, bytes, mode=None):
305
287
        resp = self._call_with_body_bytes(
306
288
            'append',
308
290
            bytes)
309
291
        if resp[0] == 'appended':
310
292
            return int(resp[1])
311
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
293
        self._translate_error(resp)
312
294
 
313
295
    def delete(self, relpath):
314
296
        resp = self._call2('delete', self._remote_path(relpath))
315
 
        self._ensure_ok(resp)
 
297
        self._translate_error(resp)
316
298
 
317
299
    def external_url(self):
318
300
        """See bzrlib.transport.Transport.external_url."""
322
304
    def recommended_page_size(self):
323
305
        """Return the recommended page size for this transport."""
324
306
        return 64 * 1024
325
 
 
 
307
        
326
308
    def _readv(self, relpath, offsets):
327
309
        if not offsets:
328
310
            return
332
314
        sorted_offsets = sorted(offsets)
333
315
        coalesced = list(self._coalesce_offsets(sorted_offsets,
334
316
                               limit=self._max_readv_combine,
335
 
                               fudge_factor=self._bytes_to_read_before_seek,
336
 
                               max_size=self._max_readv_bytes))
337
 
 
338
 
        # now that we've coallesced things, avoid making enormous requests
339
 
        requests = []
340
 
        cur_request = []
341
 
        cur_len = 0
342
 
        for c in coalesced:
343
 
            if c.length + cur_len > self._max_readv_bytes:
344
 
                requests.append(cur_request)
345
 
                cur_request = [c]
346
 
                cur_len = c.length
347
 
                continue
348
 
            cur_request.append(c)
349
 
            cur_len += c.length
350
 
        if cur_request:
351
 
            requests.append(cur_request)
352
 
        if 'hpss' in debug.debug_flags:
353
 
            trace.mutter('%s.readv %s offsets => %s coalesced'
354
 
                         ' => %s requests (%s)',
355
 
                         self.__class__.__name__, len(offsets), len(coalesced),
356
 
                         len(requests), sum(map(len, requests)))
357
 
        # Cache the results, but only until they have been fulfilled
358
 
        data_map = {}
359
 
        # turn the list of offsets into a single stack to iterate
 
317
                               fudge_factor=self._bytes_to_read_before_seek))
 
318
 
 
319
        try:
 
320
            result = self._client.call_with_body_readv_array(
 
321
                ('readv', self._remote_path(relpath),),
 
322
                [(c.start, c.length) for c in coalesced])
 
323
            resp, response_handler = result
 
324
        except errors.ErrorFromSmartServer, err:
 
325
            self._translate_error(err.error_tuple)
 
326
 
 
327
        if resp[0] != 'readv':
 
328
            # This should raise an exception
 
329
            response_handler.cancel_read_body()
 
330
            raise errors.UnexpectedSmartServerResponse(resp)
 
331
 
 
332
        return self._handle_response(offsets, coalesced, response_handler)
 
333
 
 
334
    def _handle_response(self, offsets, coalesced, response_handler):
 
335
        # turn the list of offsets into a stack
360
336
        offset_stack = iter(offsets)
361
 
        # using a list so it can be modified when passing down and coming back
362
 
        next_offset = [offset_stack.next()]
363
 
        for cur_request in requests:
364
 
            try:
365
 
                result = self._client.call_with_body_readv_array(
366
 
                    ('readv', self._remote_path(relpath),),
367
 
                    [(c.start, c.length) for c in cur_request])
368
 
                resp, response_handler = result
369
 
            except errors.ErrorFromSmartServer, err:
370
 
                self._translate_error(err, relpath)
371
 
 
372
 
            if resp[0] != 'readv':
373
 
                # This should raise an exception
374
 
                response_handler.cancel_read_body()
375
 
                raise errors.UnexpectedSmartServerResponse(resp)
376
 
 
377
 
            for res in self._handle_response(offset_stack, cur_request,
378
 
                                             response_handler,
379
 
                                             data_map,
380
 
                                             next_offset):
381
 
                yield res
382
 
 
383
 
    def _handle_response(self, offset_stack, coalesced, response_handler,
384
 
                         data_map, next_offset):
385
 
        cur_offset_and_size = next_offset[0]
 
337
        cur_offset_and_size = offset_stack.next()
386
338
        # FIXME: this should know how many bytes are needed, for clarity.
387
339
        data = response_handler.read_body_bytes()
 
340
        # Cache the results, but only until they have been fulfilled
 
341
        data_map = {}
388
342
        data_offset = 0
389
343
        for c_offset in coalesced:
390
344
            if len(data) < c_offset.length:
403
357
                #       not have a real string.
404
358
                if key == cur_offset_and_size:
405
359
                    yield cur_offset_and_size[0], this_data
406
 
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
 
360
                    cur_offset_and_size = offset_stack.next()
407
361
                else:
408
362
                    data_map[key] = this_data
409
363
            data_offset += c_offset.length
412
366
            while cur_offset_and_size in data_map:
413
367
                this_data = data_map.pop(cur_offset_and_size)
414
368
                yield cur_offset_and_size[0], this_data
415
 
                cur_offset_and_size = next_offset[0] = offset_stack.next()
 
369
                cur_offset_and_size = offset_stack.next()
416
370
 
417
371
    def rename(self, rel_from, rel_to):
418
372
        self._call('rename',
427
381
    def rmdir(self, relpath):
428
382
        resp = self._call('rmdir', self._remote_path(relpath))
429
383
 
430
 
    def _ensure_ok(self, resp):
431
 
        if resp[0] != 'ok':
432
 
            raise errors.UnexpectedSmartServerResponse(resp)
433
 
 
434
 
    def _translate_error(self, err, relpath=None):
435
 
        remote._translate_error(err, path=relpath)
 
384
    def _translate_error(self, resp, orig_path=None):
 
385
        """Raise an exception from a response"""
 
386
        if resp is None:
 
387
            what = None
 
388
        else:
 
389
            what = resp[0]
 
390
        if what == 'ok':
 
391
            return
 
392
        elif what == 'NoSuchFile':
 
393
            if orig_path is not None:
 
394
                error_path = orig_path
 
395
            else:
 
396
                error_path = resp[1]
 
397
            raise errors.NoSuchFile(error_path)
 
398
        elif what == 'error':
 
399
            raise errors.SmartProtocolError(unicode(resp[1]))
 
400
        elif what == 'FileExists':
 
401
            raise errors.FileExists(resp[1])
 
402
        elif what == 'DirectoryNotEmpty':
 
403
            raise errors.DirectoryNotEmpty(resp[1])
 
404
        elif what == 'ShortReadvError':
 
405
            raise errors.ShortReadvError(resp[1], int(resp[2]),
 
406
                                         int(resp[3]), int(resp[4]))
 
407
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
 
408
            encoding = str(resp[1]) # encoding must always be a string
 
409
            val = resp[2]
 
410
            start = int(resp[3])
 
411
            end = int(resp[4])
 
412
            reason = str(resp[5]) # reason must always be a string
 
413
            if val.startswith('u:'):
 
414
                val = val[2:].decode('utf-8')
 
415
            elif val.startswith('s:'):
 
416
                val = val[2:].decode('base64')
 
417
            if what == 'UnicodeDecodeError':
 
418
                raise UnicodeDecodeError(encoding, val, start, end, reason)
 
419
            elif what == 'UnicodeEncodeError':
 
420
                raise UnicodeEncodeError(encoding, val, start, end, reason)
 
421
        elif what == "ReadOnlyError":
 
422
            raise errors.TransportNotPossible('readonly transport')
 
423
        elif what == "ReadError":
 
424
            if orig_path is not None:
 
425
                error_path = orig_path
 
426
            else:
 
427
                error_path = resp[1]
 
428
            raise errors.ReadError(error_path)
 
429
        elif what == "PermissionDenied":
 
430
            if orig_path is not None:
 
431
                error_path = orig_path
 
432
            else:
 
433
                error_path = resp[1]
 
434
            raise errors.PermissionDenied(error_path)
 
435
        else:
 
436
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
436
437
 
437
438
    def disconnect(self):
438
 
        m = self.get_smart_medium()
439
 
        if m is not None:
440
 
            m.disconnect()
 
439
        self.get_smart_medium().disconnect()
441
440
 
442
441
    def stat(self, relpath):
443
442
        resp = self._call2('stat', self._remote_path(relpath))
444
443
        if resp[0] == 'stat':
445
444
            return _SmartStat(int(resp[1]), int(resp[2], 8))
446
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
445
        else:
 
446
            self._translate_error(resp)
447
447
 
448
448
    ## def lock_read(self, relpath):
449
449
    ##     """Lock the given file for shared (read) access.
465
465
        resp = self._call2('list_dir', self._remote_path(relpath))
466
466
        if resp[0] == 'names':
467
467
            return [name.encode('ascii') for name in resp[1:]]
468
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
468
        else:
 
469
            self._translate_error(resp)
469
470
 
470
471
    def iter_files_recursive(self):
471
472
        resp = self._call2('iter_files_recursive', self._remote_path(''))
472
473
        if resp[0] == 'names':
473
474
            return resp[1:]
474
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
475
        else:
 
476
            self._translate_error(resp)
475
477
 
476
478
 
477
479
class RemoteTCPTransport(RemoteTransport):
478
480
    """Connection to smart server over plain tcp.
479
 
 
 
481
    
480
482
    This is essentially just a factory to get 'RemoteTransport(url,
481
483
        SmartTCPClientMedium).
482
484
    """
483
485
 
484
486
    def _build_medium(self):
485
487
        client_medium = medium.SmartTCPClientMedium(
486
 
            self._parsed_url.host, self._parsed_url.port, self.base)
 
488
            self._host, self._port, self.base)
487
489
        return client_medium, None
488
490
 
489
491
 
496
498
 
497
499
    def _build_medium(self):
498
500
        client_medium = medium.SmartTCPClientMedium(
499
 
            self._parsed_url.host, self._parsed_url.port, self.base)
 
501
            self._host, self._port, self.base)
500
502
        client_medium._protocol_version = 2
501
503
        client_medium._remember_remote_is_before((1, 6))
502
504
        return client_medium, None
510
512
    """
511
513
 
512
514
    def _build_medium(self):
 
515
        # ssh will prompt the user for a password if needed and if none is
 
516
        # provided but it will not give it back, so no credentials can be
 
517
        # stored.
513
518
        location_config = config.LocationConfig(self.base)
514
519
        bzr_remote_path = location_config.get_bzr_remote_path()
515
 
        user = self._parsed_url.user
516
 
        if user is None:
517
 
            auth = config.AuthenticationConfig()
518
 
            user = auth.get_user('ssh', self._parsed_url.host,
519
 
                self._parsed_url.port)
520
 
        ssh_params = medium.SSHParams(self._parsed_url.host,
521
 
                self._parsed_url.port, user, self._parsed_url.password,
522
 
                bzr_remote_path)
523
 
        client_medium = medium.SmartSSHClientMedium(self.base, ssh_params)
524
 
        return client_medium, (user, self._parsed_url.password)
 
520
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
 
521
            self._user, self._password, self.base,
 
522
            bzr_remote_path=bzr_remote_path)
 
523
        return client_medium, None
525
524
 
526
525
 
527
526
class RemoteHTTPTransport(RemoteTransport):
528
527
    """Just a way to connect between a bzr+http:// url and http://.
529
 
 
 
528
    
530
529
    This connection operates slightly differently than the RemoteSSHTransport.
531
530
    It uses a plain http:// transport underneath, which defines what remote
532
531
    .bzr/smart URL we are connected to. From there, all paths that are sent are
581
580
                                   _from_transport=self,
582
581
                                   http_transport=self._http_transport)
583
582
 
584
 
    def _redirected_to(self, source, target):
585
 
        """See transport._redirected_to"""
586
 
        redirected = self._http_transport._redirected_to(source, target)
587
 
        if (redirected is not None
588
 
            and isinstance(redirected, type(self._http_transport))):
589
 
            return RemoteHTTPTransport('bzr+' + redirected.external_url(),
590
 
                                       http_transport=redirected)
591
 
        else:
592
 
            # Either None or a transport for a different protocol
593
 
            return redirected
594
 
 
595
 
 
596
 
class HintingSSHTransport(transport.Transport):
597
 
    """Simple transport that handles ssh:// and points out bzr+ssh://."""
598
 
 
599
 
    def __init__(self, url):
600
 
        raise errors.UnsupportedProtocol(url,
601
 
            'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
602
 
 
603
583
 
604
584
def get_test_permutations():
605
585
    """Return (transport, server) permutations for testing."""
606
586
    ### We may need a little more test framework support to construct an
607
587
    ### appropriate RemoteTransport in the future.
608
 
    from bzrlib.tests import test_server
609
 
    return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]
 
588
    from bzrlib.smart import server
 
589
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]