~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Aaron Bentley
  • Date: 2007-12-12 15:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 3113.
  • Revision ID: abentley@panoramicfeedback.com-20071212151713-ox5n8rlx8m3nsspy
Add support for reconfiguring repositories into branches or trees

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