/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: John Arbash Meinel
  • Date: 2008-05-19 20:54:37 UTC
  • mto: This revision was merged to the branch mainline in revision 3436.
  • Revision ID: john@arbash-meinel.com-20080519205437-gq0ai59wfynxhr4w
Change deprecated_in to end with a '.'

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