~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/remote.py

  • Committer: Andrew Bennetts
  • Date: 2007-04-10 05:49:02 UTC
  • mfrom: (2401 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2402.
  • Revision ID: andrew.bennetts@canonical.com-20070410054902-iphhiovkapcnzl9k
Merge from bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
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']
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24
16
 
25
17
from cStringIO import StringIO
 
18
import urllib
 
19
import urlparse
26
20
 
27
21
from bzrlib import (
28
 
    config,
29
 
    debug,
30
22
    errors,
31
 
    remote,
32
 
    trace,
33
23
    transport,
34
 
    urlutils,
35
 
    )
36
 
from bzrlib.smart import client, medium
37
 
from bzrlib.symbol_versioning import (
38
 
    deprecated_method,
39
 
    )
40
 
 
41
 
 
42
 
class _SmartStat(object):
 
24
    )
 
25
from bzrlib.smart.protocol import SmartClientRequestProtocolOne
 
26
from bzrlib.smart.medium import SmartTCPClientMedium, SmartSSHClientMedium
 
27
 
 
28
# must do this otherwise urllib can't parse the urls properly :(
 
29
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh', 'bzr+http']:
 
30
    transport.register_urlparse_netloc_protocol(scheme)
 
31
del scheme
 
32
 
 
33
 
 
34
# Port 4155 is the default port for bzr://, registered with IANA.
 
35
BZR_DEFAULT_PORT = 4155
 
36
 
 
37
 
 
38
class SmartStat(object):
43
39
 
44
40
    def __init__(self, size, mode):
45
41
        self.st_size = size
46
42
        self.st_mode = mode
47
43
 
48
44
 
49
 
class RemoteTransport(transport.ConnectedTransport):
 
45
class SmartTransport(transport.Transport):
50
46
    """Connection to a smart server.
51
47
 
52
 
    The connection holds references to the medium that can be used to send
53
 
    requests to the server.
 
48
    The connection holds references to pipes that can be used to send requests
 
49
    to the server.
54
50
 
55
51
    The connection has a notion of the current directory to which it's
56
52
    connected; this is incorporated in filenames passed to the server.
57
 
 
58
 
    This supports some higher-level RPC operations and can also be treated
 
53
    
 
54
    This supports some higher-level RPC operations and can also be treated 
59
55
    like a Transport to do file-like operations.
60
56
 
61
 
    The connection can be made over a tcp socket, an ssh pipe or a series of
62
 
    http requests.  There are concrete subclasses for each type:
63
 
    RemoteTCPTransport, etc.
 
57
    The connection can be made over a tcp socket, or (in future) an ssh pipe
 
58
    or a series of http requests.  There are concrete subclasses for each
 
59
    type: SmartTCPTransport, etc.
64
60
    """
65
61
 
66
 
    # When making a readv request, cap it at requesting 5MB of data
67
 
    _max_readv_bytes = 5*1024*1024
68
 
 
69
 
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
 
62
    # IMPORTANT FOR IMPLEMENTORS: SmartTransport MUST NOT be given encoding
70
63
    # responsibilities: Put those on SmartClient or similar. This is vital for
71
64
    # the ability to support multiple versions of the smart protocol over time:
72
 
    # RemoteTransport is an adapter from the Transport object model to the
 
65
    # SmartTransport is an adapter from the Transport object model to the 
73
66
    # SmartClient model, not an encoder.
74
67
 
75
 
    # FIXME: the medium parameter should be private, only the tests requires
76
 
    # it. It may be even clearer to define a TestRemoteTransport that handles
77
 
    # the specific cases of providing a _client and/or a _medium, and leave
78
 
    # RemoteTransport as an abstract class.
79
 
    def __init__(self, url, _from_transport=None, medium=None, _client=None):
 
68
    def __init__(self, url, clone_from=None, medium=None):
80
69
        """Constructor.
81
70
 
82
 
        :param _from_transport: Another RemoteTransport instance that this
83
 
            one is being cloned from.  Attributes such as the medium will
84
 
            be reused.
85
 
 
86
 
        :param medium: The medium to use for this RemoteTransport.  If None,
87
 
            the medium from the _from_transport is shared.  If both this
88
 
            and _from_transport are None, a new medium will be built.
89
 
            _from_transport and medium cannot both be specified.
90
 
 
91
 
        :param _client: Override the _SmartClient used by this transport.  This
92
 
            should only be used for testing purposes; normally this is
93
 
            determined from the medium.
94
 
        """
95
 
        super(RemoteTransport, self).__init__(
96
 
            url, _from_transport=_from_transport)
97
 
 
98
 
        # The medium is the connection, except when we need to share it with
99
 
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
100
 
        # what we want to share is really the shared connection.
101
 
 
102
 
        if (_from_transport is not None
103
 
            and isinstance(_from_transport, RemoteTransport)):
104
 
            _client = _from_transport._client
105
 
        elif _from_transport is None:
106
 
            # If no _from_transport is specified, we need to intialize the
107
 
            # shared medium.
108
 
            credentials = None
109
 
            if medium is None:
110
 
                medium, credentials = self._build_medium()
111
 
                if 'hpss' in debug.debug_flags:
112
 
                    trace.mutter('hpss: Built a new medium: %s',
113
 
                                 medium.__class__.__name__)
114
 
            self._shared_connection = transport._SharedConnection(medium,
115
 
                                                                  credentials,
116
 
                                                                  self.base)
117
 
        elif medium is None:
118
 
            # No medium was specified, so share the medium from the
119
 
            # _from_transport.
120
 
            medium = self._shared_connection.connection
121
 
        else:
122
 
            raise AssertionError(
123
 
                "Both _from_transport (%r) and medium (%r) passed to "
124
 
                "RemoteTransport.__init__, but these parameters are mutally "
125
 
                "exclusive." % (_from_transport, medium))
126
 
 
127
 
        if _client is None:
128
 
            self._client = client._SmartClient(medium)
129
 
        else:
130
 
            self._client = _client
131
 
 
132
 
    def _build_medium(self):
133
 
        """Create the medium if _from_transport does not provide one.
134
 
 
135
 
        The medium is analogous to the connection for ConnectedTransport: it
136
 
        allows connection sharing.
137
 
        """
138
 
        # No credentials
139
 
        return None, None
140
 
 
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
 
71
        :param medium: The medium to use for this RemoteTransport. This must be
 
72
            supplied if clone_from is None.
 
73
        """
 
74
        ### Technically super() here is faulty because Transport's __init__
 
75
        ### fails to take 2 parameters, and if super were to choose a silly
 
76
        ### initialisation order things would blow up. 
 
77
        if not url.endswith('/'):
 
78
            url += '/'
 
79
        super(SmartTransport, self).__init__(url)
 
80
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
 
81
                transport.split_url(url)
 
82
        if clone_from is None:
 
83
            self._medium = medium
 
84
        else:
 
85
            # credentials may be stripped from the base in some circumstances
 
86
            # as yet to be clearly defined or documented, so copy them.
 
87
            self._username = clone_from._username
 
88
            # reuse same connection
 
89
            self._medium = clone_from._medium
 
90
        assert self._medium is not None
 
91
 
 
92
    def abspath(self, relpath):
 
93
        """Return the full url to the given relative path.
 
94
        
 
95
        @param relpath: the relative path or path components
 
96
        @type relpath: str or list
 
97
        """
 
98
        return self._unparse_url(self._remote_path(relpath))
 
99
    
 
100
    def clone(self, relative_url):
 
101
        """Make a new SmartTransport related to me, sharing the same connection.
 
102
 
 
103
        This essentially opens a handle on a different remote directory.
 
104
        """
 
105
        if relative_url is None:
 
106
            return SmartTransport(self.base, self)
 
107
        else:
 
108
            return SmartTransport(self.abspath(relative_url), self)
148
109
 
149
110
    def is_readonly(self):
150
111
        """Smart server transport can do read/write file operations."""
151
 
        try:
152
 
            resp = self._call2('Transport.is_readonly')
153
 
        except errors.UnknownSmartMethod:
154
 
            # XXX: nasty hack: servers before 0.16 don't have a
155
 
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
156
 
            # did: assume False.
157
 
            return False
158
 
        if resp == ('yes', ):
159
 
            return True
160
 
        elif resp == ('no', ):
161
 
            return False
162
 
        else:
163
 
            raise errors.UnexpectedSmartServerResponse(resp)
164
 
 
 
112
        return False
 
113
                                                   
165
114
    def get_smart_client(self):
166
 
        return self._get_connection()
 
115
        return self._medium
167
116
 
168
117
    def get_smart_medium(self):
169
 
        return self._get_connection()
 
118
        return self._medium
 
119
                                                   
 
120
    def _unparse_url(self, path):
 
121
        """Return URL for a path.
 
122
 
 
123
        :see: SFTPUrlHandling._unparse_url
 
124
        """
 
125
        # TODO: Eventually it should be possible to unify this with
 
126
        # SFTPUrlHandling._unparse_url?
 
127
        if path == '':
 
128
            path = '/'
 
129
        path = urllib.quote(path)
 
130
        netloc = urllib.quote(self._host)
 
131
        if self._username is not None:
 
132
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
133
        if self._port is not None:
 
134
            netloc = '%s:%d' % (netloc, self._port)
 
135
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
170
136
 
171
137
    def _remote_path(self, relpath):
172
138
        """Returns the Unicode version of the absolute path for relpath."""
174
140
 
175
141
    def _call(self, method, *args):
176
142
        resp = self._call2(method, *args)
177
 
        self._ensure_ok(resp)
 
143
        self._translate_error(resp)
178
144
 
179
145
    def _call2(self, method, *args):
180
146
        """Call a method on the remote server."""
181
 
        try:
182
 
            return self._client.call(method, *args)
183
 
        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)
 
147
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
 
148
        protocol.call(method, *args)
 
149
        return protocol.read_response_tuple()
190
150
 
191
151
    def _call_with_body_bytes(self, method, args, body):
192
152
        """Call a method on the remote server with body bytes."""
193
 
        try:
194
 
            return self._client.call_with_body_bytes(method, args, body)
195
 
        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)
 
153
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
 
154
        protocol.call_with_body_bytes((method, ) + args, body)
 
155
        return protocol.read_response_tuple()
202
156
 
203
157
    def has(self, relpath):
204
158
        """Indicate whether a remote file of the given name exists or not.
211
165
        elif resp == ('no', ):
212
166
            return False
213
167
        else:
214
 
            raise errors.UnexpectedSmartServerResponse(resp)
 
168
            self._translate_error(resp)
215
169
 
216
170
    def get(self, relpath):
217
171
        """Return file-like object reading the contents of a remote file.
218
 
 
 
172
        
219
173
        :see: Transport.get_bytes()/get_file()
220
174
        """
221
175
        return StringIO(self.get_bytes(relpath))
222
176
 
223
177
    def get_bytes(self, relpath):
224
178
        remote = self._remote_path(relpath)
225
 
        try:
226
 
            resp, response_handler = self._client.call_expecting_body('get', remote)
227
 
        except errors.ErrorFromSmartServer, err:
228
 
            self._translate_error(err, relpath)
 
179
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
 
180
        protocol.call('get', remote)
 
181
        resp = protocol.read_response_tuple(True)
229
182
        if resp != ('ok', ):
230
 
            response_handler.cancel_read_body()
231
 
            raise errors.UnexpectedSmartServerResponse(resp)
232
 
        return response_handler.read_body_bytes()
 
183
            protocol.cancel_read_body()
 
184
            self._translate_error(resp, relpath)
 
185
        return protocol.read_body_bytes()
233
186
 
234
187
    def _serialise_optional_mode(self, mode):
235
188
        if mode is None:
240
193
    def mkdir(self, relpath, mode=None):
241
194
        resp = self._call2('mkdir', self._remote_path(relpath),
242
195
            self._serialise_optional_mode(mode))
243
 
 
244
 
    def open_write_stream(self, relpath, mode=None):
245
 
        """See Transport.open_write_stream."""
246
 
        self.put_bytes(relpath, "", mode)
247
 
        result = transport.AppendBasedFileStream(self, relpath)
248
 
        transport._file_streams[self.abspath(relpath)] = result
249
 
        return result
 
196
        self._translate_error(resp)
250
197
 
251
198
    def put_bytes(self, relpath, upload_contents, mode=None):
252
199
        # FIXME: upload_file is probably not safe for non-ascii characters -
253
200
        # should probably just pass all parameters as length-delimited
254
201
        # strings?
255
 
        if type(upload_contents) is unicode:
256
 
            # Although not strictly correct, we raise UnicodeEncodeError to be
257
 
            # compatible with other transports.
258
 
            raise UnicodeEncodeError(
259
 
                'undefined', upload_contents, 0, 1,
260
 
                'put_bytes must be given bytes, not unicode.')
261
202
        resp = self._call_with_body_bytes('put',
262
203
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
263
204
            upload_contents)
264
 
        self._ensure_ok(resp)
265
 
        return len(upload_contents)
 
205
        self._translate_error(resp)
266
206
 
267
207
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
268
208
                             create_parent_dir=False,
278
218
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
279
219
             create_parent_str, self._serialise_optional_mode(dir_mode)),
280
220
            bytes)
281
 
        self._ensure_ok(resp)
 
221
        self._translate_error(resp)
282
222
 
283
223
    def put_file(self, relpath, upload_file, mode=None):
284
224
        # its not ideal to seek back, but currently put_non_atomic_file depends
300
240
 
301
241
    def append_file(self, relpath, from_file, mode=None):
302
242
        return self.append_bytes(relpath, from_file.read(), mode)
303
 
 
 
243
        
304
244
    def append_bytes(self, relpath, bytes, mode=None):
305
245
        resp = self._call_with_body_bytes(
306
246
            'append',
308
248
            bytes)
309
249
        if resp[0] == 'appended':
310
250
            return int(resp[1])
311
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
251
        self._translate_error(resp)
312
252
 
313
253
    def delete(self, relpath):
314
254
        resp = self._call2('delete', self._remote_path(relpath))
315
 
        self._ensure_ok(resp)
316
 
 
317
 
    def external_url(self):
318
 
        """See bzrlib.transport.Transport.external_url."""
319
 
        # the external path for RemoteTransports is the base
320
 
        return self.base
321
 
 
322
 
    def recommended_page_size(self):
323
 
        """Return the recommended page size for this transport."""
324
 
        return 64 * 1024
325
 
 
326
 
    def _readv(self, relpath, offsets):
 
255
        self._translate_error(resp)
 
256
 
 
257
    def readv(self, relpath, offsets):
327
258
        if not offsets:
328
259
            return
329
260
 
330
261
        offsets = list(offsets)
331
262
 
332
263
        sorted_offsets = sorted(offsets)
 
264
        # turn the list of offsets into a stack
 
265
        offset_stack = iter(offsets)
 
266
        cur_offset_and_size = offset_stack.next()
333
267
        coalesced = list(self._coalesce_offsets(sorted_offsets,
334
268
                               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)))
 
269
                               fudge_factor=self._bytes_to_read_before_seek))
 
270
 
 
271
        protocol = SmartClientRequestProtocolOne(self._medium.get_request())
 
272
        protocol.call_with_body_readv_array(
 
273
            ('readv', self._remote_path(relpath)),
 
274
            [(c.start, c.length) for c in coalesced])
 
275
        resp = protocol.read_response_tuple(True)
 
276
 
 
277
        if resp[0] != 'readv':
 
278
            # This should raise an exception
 
279
            protocol.cancel_read_body()
 
280
            self._translate_error(resp)
 
281
            return
 
282
 
 
283
        # FIXME: this should know how many bytes are needed, for clarity.
 
284
        data = protocol.read_body_bytes()
357
285
        # Cache the results, but only until they have been fulfilled
358
286
        data_map = {}
359
 
        # turn the list of offsets into a single stack to iterate
360
 
        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]
386
 
        # FIXME: this should know how many bytes are needed, for clarity.
387
 
        data = response_handler.read_body_bytes()
388
 
        data_offset = 0
389
287
        for c_offset in coalesced:
390
288
            if len(data) < c_offset.length:
391
289
                raise errors.ShortReadvError(relpath, c_offset.start,
392
290
                            c_offset.length, actual=len(data))
393
291
            for suboffset, subsize in c_offset.ranges:
394
292
                key = (c_offset.start+suboffset, subsize)
395
 
                this_data = data[data_offset+suboffset:
396
 
                                 data_offset+suboffset+subsize]
397
 
                # Special case when the data is in-order, rather than packing
398
 
                # into a map and then back out again. Benchmarking shows that
399
 
                # this has 100% hit rate, but leave in the data_map work just
400
 
                # in case.
401
 
                # TODO: Could we get away with using buffer() to avoid the
402
 
                #       memory copy?  Callers would need to realize they may
403
 
                #       not have a real string.
404
 
                if key == cur_offset_and_size:
405
 
                    yield cur_offset_and_size[0], this_data
406
 
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
407
 
                else:
408
 
                    data_map[key] = this_data
409
 
            data_offset += c_offset.length
 
293
                data_map[key] = data[suboffset:suboffset+subsize]
 
294
            data = data[c_offset.length:]
410
295
 
411
296
            # Now that we've read some data, see if we can yield anything back
412
297
            while cur_offset_and_size in data_map:
413
298
                this_data = data_map.pop(cur_offset_and_size)
414
299
                yield cur_offset_and_size[0], this_data
415
 
                cur_offset_and_size = next_offset[0] = offset_stack.next()
 
300
                cur_offset_and_size = offset_stack.next()
416
301
 
417
302
    def rename(self, rel_from, rel_to):
418
303
        self._call('rename',
427
312
    def rmdir(self, relpath):
428
313
        resp = self._call('rmdir', self._remote_path(relpath))
429
314
 
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)
 
315
    def _translate_error(self, resp, orig_path=None):
 
316
        """Raise an exception from a response"""
 
317
        if resp is None:
 
318
            what = None
 
319
        else:
 
320
            what = resp[0]
 
321
        if what == 'ok':
 
322
            return
 
323
        elif what == 'NoSuchFile':
 
324
            if orig_path is not None:
 
325
                error_path = orig_path
 
326
            else:
 
327
                error_path = resp[1]
 
328
            raise errors.NoSuchFile(error_path)
 
329
        elif what == 'error':
 
330
            raise errors.SmartProtocolError(unicode(resp[1]))
 
331
        elif what == 'FileExists':
 
332
            raise errors.FileExists(resp[1])
 
333
        elif what == 'DirectoryNotEmpty':
 
334
            raise errors.DirectoryNotEmpty(resp[1])
 
335
        elif what == 'ShortReadvError':
 
336
            raise errors.ShortReadvError(resp[1], int(resp[2]),
 
337
                                         int(resp[3]), int(resp[4]))
 
338
        elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
 
339
            encoding = str(resp[1]) # encoding must always be a string
 
340
            val = resp[2]
 
341
            start = int(resp[3])
 
342
            end = int(resp[4])
 
343
            reason = str(resp[5]) # reason must always be a string
 
344
            if val.startswith('u:'):
 
345
                val = val[2:].decode('utf-8')
 
346
            elif val.startswith('s:'):
 
347
                val = val[2:].decode('base64')
 
348
            if what == 'UnicodeDecodeError':
 
349
                raise UnicodeDecodeError(encoding, val, start, end, reason)
 
350
            elif what == 'UnicodeEncodeError':
 
351
                raise UnicodeEncodeError(encoding, val, start, end, reason)
 
352
        elif what == "ReadOnlyError":
 
353
            raise errors.TransportNotPossible('readonly transport')
 
354
        else:
 
355
            raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
436
356
 
437
357
    def disconnect(self):
438
 
        self.get_smart_medium().disconnect()
 
358
        self._medium.disconnect()
 
359
 
 
360
    def delete_tree(self, relpath):
 
361
        raise errors.TransportNotPossible('readonly transport')
439
362
 
440
363
    def stat(self, relpath):
441
364
        resp = self._call2('stat', self._remote_path(relpath))
442
365
        if resp[0] == 'stat':
443
 
            return _SmartStat(int(resp[1]), int(resp[2], 8))
444
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
366
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
367
        else:
 
368
            self._translate_error(resp)
445
369
 
446
370
    ## def lock_read(self, relpath):
447
371
    ##     """Lock the given file for shared (read) access.
463
387
        resp = self._call2('list_dir', self._remote_path(relpath))
464
388
        if resp[0] == 'names':
465
389
            return [name.encode('ascii') for name in resp[1:]]
466
 
        raise errors.UnexpectedSmartServerResponse(resp)
 
390
        else:
 
391
            self._translate_error(resp)
467
392
 
468
393
    def iter_files_recursive(self):
469
394
        resp = self._call2('iter_files_recursive', self._remote_path(''))
470
395
        if resp[0] == 'names':
471
396
            return resp[1:]
472
 
        raise errors.UnexpectedSmartServerResponse(resp)
473
 
 
474
 
 
475
 
class RemoteTCPTransport(RemoteTransport):
 
397
        else:
 
398
            self._translate_error(resp)
 
399
 
 
400
 
 
401
 
 
402
class SmartTCPTransport(SmartTransport):
476
403
    """Connection to smart server over plain tcp.
477
 
 
 
404
    
478
405
    This is essentially just a factory to get 'RemoteTransport(url,
479
406
        SmartTCPClientMedium).
480
407
    """
481
408
 
482
 
    def _build_medium(self):
483
 
        client_medium = medium.SmartTCPClientMedium(
484
 
            self._host, self._port, self.base)
485
 
        return client_medium, None
486
 
 
487
 
 
488
 
class RemoteTCPTransportV2Only(RemoteTransport):
489
 
    """Connection to smart server over plain tcp with the client hard-coded to
490
 
    assume protocol v2 and remote server version <= 1.6.
491
 
 
492
 
    This should only be used for testing.
493
 
    """
494
 
 
495
 
    def _build_medium(self):
496
 
        client_medium = medium.SmartTCPClientMedium(
497
 
            self._host, self._port, self.base)
498
 
        client_medium._protocol_version = 2
499
 
        client_medium._remember_remote_is_before((1, 6))
500
 
        return client_medium, None
501
 
 
502
 
 
503
 
class RemoteSSHTransport(RemoteTransport):
 
409
    def __init__(self, url):
 
410
        _scheme, _username, _password, _host, _port, _path = \
 
411
            transport.split_url(url)
 
412
        if _port is None:
 
413
            _port = BZR_DEFAULT_PORT
 
414
        else:
 
415
            try:
 
416
                _port = int(_port)
 
417
            except (ValueError, TypeError), e:
 
418
                raise errors.InvalidURL(
 
419
                    path=url, extra="invalid port %s" % _port)
 
420
        medium = SmartTCPClientMedium(_host, _port)
 
421
        super(SmartTCPTransport, self).__init__(url, medium=medium)
 
422
 
 
423
 
 
424
class SmartSSHTransport(SmartTransport):
504
425
    """Connection to smart server over SSH.
505
426
 
506
427
    This is essentially just a factory to get 'RemoteTransport(url,
507
428
        SmartSSHClientMedium).
508
429
    """
509
430
 
510
 
    def _build_medium(self):
511
 
        location_config = config.LocationConfig(self.base)
512
 
        bzr_remote_path = location_config.get_bzr_remote_path()
513
 
        user = self._user
514
 
        if user is None:
515
 
            auth = config.AuthenticationConfig()
516
 
            user = auth.get_user('ssh', self._host, self._port)
517
 
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
518
 
            user, self._password, self.base,
519
 
            bzr_remote_path=bzr_remote_path)
520
 
        return client_medium, (user, self._password)
521
 
 
522
 
 
523
 
class RemoteHTTPTransport(RemoteTransport):
 
431
    def __init__(self, url):
 
432
        _scheme, _username, _password, _host, _port, _path = \
 
433
            transport.split_url(url)
 
434
        try:
 
435
            if _port is not None:
 
436
                _port = int(_port)
 
437
        except (ValueError, TypeError), e:
 
438
            raise errors.InvalidURL(path=url, extra="invalid port %s" % 
 
439
                _port)
 
440
        medium = SmartSSHClientMedium(_host, _port, _username, _password)
 
441
        super(SmartSSHTransport, self).__init__(url, medium=medium)
 
442
 
 
443
 
 
444
class SmartHTTPTransport(SmartTransport):
524
445
    """Just a way to connect between a bzr+http:// url and http://.
525
 
 
526
 
    This connection operates slightly differently than the RemoteSSHTransport.
 
446
    
 
447
    This connection operates slightly differently than the SmartSSHTransport.
527
448
    It uses a plain http:// transport underneath, which defines what remote
528
449
    .bzr/smart URL we are connected to. From there, all paths that are sent are
529
450
    sent as relative paths, this way, the remote side can properly
531
452
    HTTP path into a local path.
532
453
    """
533
454
 
534
 
    def __init__(self, base, _from_transport=None, http_transport=None):
 
455
    def __init__(self, url, http_transport=None):
 
456
        assert url.startswith('bzr+http://')
 
457
 
535
458
        if http_transport is None:
536
 
            # FIXME: the password may be lost here because it appears in the
537
 
            # url only for an intial construction (when the url came from the
538
 
            # command-line).
539
 
            http_url = base[len('bzr+'):]
 
459
            http_url = url[len('bzr+'):]
540
460
            self._http_transport = transport.get_transport(http_url)
541
461
        else:
542
462
            self._http_transport = http_transport
543
 
        super(RemoteHTTPTransport, self).__init__(
544
 
            base, _from_transport=_from_transport)
545
 
 
546
 
    def _build_medium(self):
547
 
        # We let http_transport take care of the credentials
548
 
        return self._http_transport.get_smart_medium(), None
 
463
        http_medium = self._http_transport.get_smart_medium()
 
464
        super(SmartHTTPTransport, self).__init__(url, medium=http_medium)
549
465
 
550
466
    def _remote_path(self, relpath):
551
 
        """After connecting, HTTP Transport only deals in relative URLs."""
552
 
        # Adjust the relpath based on which URL this smart transport is
553
 
        # connected to.
554
 
        http_base = urlutils.normalize_url(self.get_smart_medium().base)
555
 
        url = urlutils.join(self.base[len('bzr+'):], relpath)
556
 
        url = urlutils.normalize_url(url)
557
 
        return urlutils.relative_url(http_base, url)
 
467
        """After connecting HTTP Transport only deals in relative URLs."""
 
468
        if relpath == '.':
 
469
            return ''
 
470
        else:
 
471
            return relpath
 
472
 
 
473
    def abspath(self, relpath):
 
474
        """Return the full url to the given relative path.
 
475
        
 
476
        :param relpath: the relative path or path components
 
477
        :type relpath: str or list
 
478
        """
 
479
        return self._unparse_url(self._combine_paths(self._path, relpath))
558
480
 
559
481
    def clone(self, relative_url):
560
 
        """Make a new RemoteHTTPTransport related to me.
 
482
        """Make a new SmartHTTPTransport related to me.
561
483
 
562
484
        This is re-implemented rather than using the default
563
 
        RemoteTransport.clone() because we must be careful about the underlying
 
485
        SmartTransport.clone() because we must be careful about the underlying
564
486
        http transport.
565
 
 
566
 
        Also, the cloned smart transport will POST to the same .bzr/smart
567
 
        location as this transport (although obviously the relative paths in the
568
 
        smart requests may be different).  This is so that the server doesn't
569
 
        have to handle .bzr/smart requests at arbitrary places inside .bzr
570
 
        directories, just at the initial URL the user uses.
571
487
        """
572
488
        if relative_url:
573
489
            abs_url = self.abspath(relative_url)
574
490
        else:
575
491
            abs_url = self.base
576
 
        return RemoteHTTPTransport(abs_url,
577
 
                                   _from_transport=self,
578
 
                                   http_transport=self._http_transport)
579
 
 
580
 
    def _redirected_to(self, source, target):
581
 
        """See transport._redirected_to"""
582
 
        redirected = self._http_transport._redirected_to(source, target)
583
 
        if (redirected is not None
584
 
            and isinstance(redirected, type(self._http_transport))):
585
 
            return RemoteHTTPTransport('bzr+' + redirected.external_url(),
586
 
                                       http_transport=redirected)
587
 
        else:
588
 
            # Either None or a transport for a different protocol
589
 
            return redirected
590
 
 
591
 
 
592
 
class HintingSSHTransport(transport.Transport):
593
 
    """Simple transport that handles ssh:// and points out bzr+ssh://."""
594
 
 
595
 
    def __init__(self, url):
596
 
        raise errors.UnsupportedProtocol(url,
597
 
            'bzr supports bzr+ssh to operate over ssh, use "bzr+%s".' % url)
 
492
        # By cloning the underlying http_transport, we are able to share the
 
493
        # connection.
 
494
        new_transport = self._http_transport.clone(relative_url)
 
495
        return SmartHTTPTransport(abs_url, http_transport=new_transport)
598
496
 
599
497
 
600
498
def get_test_permutations():
601
499
    """Return (transport, server) permutations for testing."""
 
500
    from bzrlib.smart import server
602
501
    ### We may need a little more test framework support to construct an
603
502
    ### appropriate RemoteTransport in the future.
604
 
    from bzrlib.tests import test_server
605
 
    return [(RemoteTCPTransport, test_server.SmartTCPServer_for_testing)]
 
503
    return [(SmartTCPTransport, server.SmartTCPServer_for_testing)]