/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: Martin Albisetti
  • Date: 2008-05-06 00:45:01 UTC
  • mto: (3431.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 3432.
  • Revision ID: argentina@gmail.com-20080506004501-h8gdmmbgnips2td7
Added link to spanish docs in the index

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