/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/smart.py

Merge from bzr.dev

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
"""Smart-server protocol, client and server.
 
18
 
 
19
Requests are sent as a command and list of arguments, followed by optional
 
20
bulk body data.  Responses are similarly a response and list of arguments,
 
21
followed by bulk body data. ::
 
22
 
 
23
  SEP := '\001'
 
24
    Fields are separated by Ctrl-A.
 
25
  BULK_DATA := CHUNK+ TRAILER
 
26
    Chunks can be repeated as many times as necessary.
 
27
  CHUNK := CHUNK_LEN CHUNK_BODY
 
28
  CHUNK_LEN := DIGIT+ NEWLINE
 
29
    Gives the number of bytes in the following chunk.
 
30
  CHUNK_BODY := BYTE[chunk_len]
 
31
  TRAILER := SUCCESS_TRAILER | ERROR_TRAILER
 
32
  SUCCESS_TRAILER := 'done' NEWLINE
 
33
  ERROR_TRAILER := 
 
34
 
 
35
Paths are passed across the network.  The client needs to see a namespace that
 
36
includes any repository that might need to be referenced, and the client needs
 
37
to know about a root directory beyond which it cannot ascend.
 
38
 
 
39
Servers run over ssh will typically want to be able to access any path the user 
 
40
can access.  Public servers on the other hand (which might be over http, ssh
 
41
or tcp) will typically want to restrict access to only a particular directory 
 
42
and its children, so will want to do a software virtual root at that level.
 
43
In other words they'll want to rewrite incoming paths to be under that level
 
44
(and prevent escaping using ../ tricks.)
 
45
 
 
46
URLs that include ~ should probably be passed across to the server verbatim
 
47
and the server can expand them.  This will proably not be meaningful when 
 
48
limited to a directory?
 
49
"""
 
50
 
 
51
 
 
52
 
 
53
# TODO: A plain integer from query_version is too simple; should give some
 
54
# capabilities too?
 
55
 
 
56
# TODO: Server should probably catch exceptions within itself and send them
 
57
# back across the network.  (But shouldn't catch KeyboardInterrupt etc)
 
58
# Also needs to somehow report protocol errors like bad requests.  Need to
 
59
# consider how we'll handle error reporting, e.g. if we get halfway through a
 
60
# bulk transfer and then something goes wrong.
 
61
 
 
62
# TODO: Standard marker at start of request/response lines?
 
63
 
 
64
# TODO: Client and server warnings perhaps should contain some non-ascii bytes
 
65
# to make sure the channel can carry them without trouble?  Test for this?
 
66
#
 
67
# TODO: get/put objects could be changed to gradually read back the data as it
 
68
# comes across the network
 
69
#
 
70
# TODO: What should the server do if it hits an error and has to terminate?
 
71
#
 
72
# TODO: is it useful to allow multiple chunks in the bulk data?
 
73
#
 
74
# TODO: If we get an exception during transmission of bulk data we can't just
 
75
# emit the exception because it won't be seen.
 
76
#
 
77
# TODO: Clone method on Transport; should work up towards parent directory;
 
78
# unclear how this should be stored or communicated to the server... maybe
 
79
# just pass it on all relevant requests?
 
80
#
 
81
# TODO: Better name than clone() for changing between directories.  How about
 
82
# open_dir or change_dir or chdir?
 
83
#
 
84
# TODO: Is it really good to have the notion of current directory within the
 
85
# connection?  Perhaps all Transports should factor out a common connection
 
86
# from the thing that has the directory context?
 
87
#
 
88
# TODO: Pull more things common to sftp and ssh to a higher level.
 
89
#
 
90
# TODO: The server that manages a connection should be quite small and retain
 
91
# minimum state because each of the requests are supposed to be stateless.
 
92
# Then we can write another implementation that maps to http.
 
93
#
 
94
# TODO: What to do when a client connection is garbage collected?  Maybe just
 
95
# abruptly drop the connection?
 
96
#
 
97
# TODO: Server in some cases will need to restrict access to files outside of
 
98
# a particular root directory.  LocalTransport doesn't do anything to stop you
 
99
# ascending above the base directory, so we need to prevent paths
 
100
# containing '..' in either the server or transport layers.  (Also need to
 
101
# consider what happens if someone creates a symlink pointing outside the 
 
102
# directory tree...)
 
103
#
 
104
# TODO: Server should rebase absolute paths coming across the network to put
 
105
# them under the virtual root, if one is in use.  LocalTransport currently
 
106
# doesn't do that; if you give it an absolute path it just uses it.
 
107
 
108
# XXX: Arguments can't contain newlines or ascii; possibly we should e.g.
 
109
# urlescape them instead.  Indeed possibly this should just literally be
 
110
# http-over-ssh.
 
111
#
 
112
# FIXME: This transport, with several others, has imperfect handling of paths
 
113
# within urls.  It'd probably be better for ".." from a root to raise an error
 
114
# rather than return the same directory as we do at present.
 
115
#
 
116
# TODO: Rather than working at the Transport layer we want a Branch,
 
117
# Repository or BzrDir objects that talk to a server.
 
118
#
 
119
# TODO: Probably want some way for server commands to gradually produce body
 
120
# data rather than passing it as a string; they could perhaps pass an
 
121
# iterator-like callback that will gradually yield data; it probably needs a
 
122
# close() method that will always be closed to do any necessary cleanup.
 
123
#
 
124
# TODO: Transport should probably not implicitly connect from its constructor;
 
125
# it's quite useful to be able to deal with Transports representing locations
 
126
# without actually opening it.
 
127
#
 
128
# TODO: Split the actual smart server from the ssh encoding of it.
 
129
#
 
130
# TODO: Perhaps support file-level readwrite operations over the transport
 
131
# too.
 
132
#
 
133
# TODO: SmartBzrDir class, proxying all Branch etc methods across to another
 
134
# branch doing file-level operations.
 
135
 
 
136
 
 
137
from cStringIO import StringIO
 
138
import errno
 
139
import os
 
140
import socket
 
141
import sys
 
142
import tempfile
 
143
import threading
 
144
import urllib
 
145
import urlparse
 
146
 
 
147
from bzrlib import bzrdir, errors, revision, transport, trace, urlutils
 
148
from bzrlib.transport import sftp, local
 
149
from bzrlib.bundle.serializer import write_bundle
 
150
from bzrlib.trace import mutter
 
151
 
 
152
# must do this otherwise urllib can't parse the urls properly :(
 
153
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh']:
 
154
    transport.register_urlparse_netloc_protocol(scheme)
 
155
del scheme
 
156
 
 
157
 
 
158
class BzrProtocolError(errors.TransportError):
 
159
    """Generic bzr smart protocol error: %(details)s"""
 
160
 
 
161
    def __init__(self, details):
 
162
        self.details = details
 
163
 
 
164
 
 
165
def _recv_tuple(from_file):
 
166
    req_line = from_file.readline()
 
167
    return _decode_tuple(req_line)
 
168
 
 
169
 
 
170
def _decode_tuple(req_line):
 
171
    if req_line == None or req_line == '':
 
172
        return None
 
173
    if req_line[-1] != '\n':
 
174
        raise BzrProtocolError("request %r not terminated" % req_line)
 
175
    return tuple((a.decode('utf-8') for a in req_line[:-1].split('\1')))
 
176
 
 
177
 
 
178
def _send_tuple(to_file, args):
 
179
    to_file.write('\1'.join((a.encode('utf-8') for a in args)) + '\n')
 
180
    to_file.flush()
 
181
 
 
182
 
 
183
class SmartProtocolBase(object):
 
184
    """Methods common to client and server"""
 
185
 
 
186
    def _send_bulk_data(self, body):
 
187
        """Send chunked body data"""
 
188
        assert isinstance(body, str)
 
189
        self._out.write('%d\n' % len(body))
 
190
        self._out.write(body)
 
191
        self._out.write('done\n')
 
192
        self._out.flush()
 
193
 
 
194
    # TODO: this only actually accomodates a single block; possibly should support
 
195
    # multiple chunks?
 
196
    def _recv_bulk(self):
 
197
        chunk_len = self._in.readline()
 
198
        try:
 
199
            chunk_len = int(chunk_len)
 
200
        except ValueError:
 
201
            raise BzrProtocolError("bad chunk length line %r" % chunk_len)
 
202
        bulk = self._in.read(chunk_len)
 
203
        if len(bulk) != chunk_len:
 
204
            raise BzrProtocolError("short read fetching bulk data chunk")
 
205
        self._recv_trailer()
 
206
        return bulk
 
207
 
 
208
    def _recv_tuple(self):
 
209
        return _recv_tuple(self._in)
 
210
 
 
211
    def _recv_trailer(self):
 
212
        resp = self._recv_tuple()
 
213
        if resp == ('done', ):
 
214
            return
 
215
        else:
 
216
            self._translate_error(resp)
 
217
 
 
218
 
 
219
class SmartStreamServer(SmartProtocolBase):
 
220
    """Handles smart commands coming over a stream.
 
221
 
 
222
    The stream may be a pipe connected to sshd, or a tcp socket, or an
 
223
    in-process fifo for testing.
 
224
 
 
225
    One instance is created for each connected client; it can serve multiple
 
226
    requests in the lifetime of the connection.
 
227
 
 
228
    The server passes requests through to an underlying backing transport, 
 
229
    which will typically be a LocalTransport looking at the server's filesystem.
 
230
    """
 
231
 
 
232
    def __init__(self, in_file, out_file, backing_transport):
 
233
        """Construct new server.
 
234
 
 
235
        :param in_file: Python file from which requests can be read.
 
236
        :param out_file: Python file to write responses.
 
237
        :param backing_transport: Transport for the directory served.
 
238
        """
 
239
        self._in = in_file
 
240
        self._out = out_file
 
241
        self.smart_server = SmartServer(backing_transport)
 
242
        # server can call back to us to get bulk data - this is not really
 
243
        # ideal, they should get it per request instead
 
244
        self.smart_server._recv_body = self._recv_bulk
 
245
 
 
246
    def _recv_tuple(self):
 
247
        """Read a request from the client and return as a tuple.
 
248
        
 
249
        Returns None at end of file (if the client closed the connection.)
 
250
        """
 
251
        return _recv_tuple(self._in)
 
252
 
 
253
    def _send_tuple(self, args):
 
254
        """Send response header"""
 
255
        return _send_tuple(self._out, args)
 
256
 
 
257
    def _send_error_and_disconnect(self, exception):
 
258
        self._send_tuple(('error', str(exception)))
 
259
        self._out.flush()
 
260
        ## self._out.close()
 
261
        ## self._in.close()
 
262
 
 
263
    def _serve_one_request(self):
 
264
        """Read one request from input, process, send back a response.
 
265
        
 
266
        :return: False if the server should terminate, otherwise None.
 
267
        """
 
268
        req_args = self._recv_tuple()
 
269
        if req_args == None:
 
270
            # client closed connection
 
271
            return False  # shutdown server
 
272
        try:
 
273
            response = self.smart_server.dispatch_command(req_args[0], req_args[1:])
 
274
            self._send_tuple(response.args)
 
275
            if response.body is not None:
 
276
                self._send_bulk_data(response.body)
 
277
        except KeyboardInterrupt:
 
278
            raise
 
279
        except Exception, e:
 
280
            # everything else: pass to client, flush, and quit
 
281
            self._send_error_and_disconnect(e)
 
282
            return False
 
283
 
 
284
    def serve(self):
 
285
        """Serve requests until the client disconnects."""
 
286
        # Keep a reference to stderr because the sys module's globals get set to
 
287
        # None during interpreter shutdown.
 
288
        from sys import stderr
 
289
        try:
 
290
            while self._serve_one_request() != False:
 
291
                pass
 
292
        except Exception, e:
 
293
            stderr.write("%s terminating on exception %s\n" % (self, e))
 
294
            raise
 
295
 
 
296
 
 
297
class SmartServerResponse(object):
 
298
    """Response generated by SmartServer."""
 
299
 
 
300
    def __init__(self, args, body=None):
 
301
        self.args = args
 
302
        self.body = body
 
303
 
 
304
 
 
305
class SmartServer(object):
 
306
    """Protocol logic for smart.
 
307
    
 
308
    This doesn't handle serialization at all, it just processes requests and
 
309
    creates responses.
 
310
    """
 
311
 
 
312
    # TODO: Better way of representing the body for commands that take it,
 
313
    # and allow it to be streamed into the server.
 
314
    
 
315
    def __init__(self, backing_transport):
 
316
        self._backing_transport = backing_transport
 
317
        
 
318
    def do_hello(self):
 
319
        """Answer a version request with my version."""
 
320
        return SmartServerResponse(('ok', '1'))
 
321
 
 
322
    def do_has(self, relpath):
 
323
        r = self._backing_transport.has(relpath) and 'yes' or 'no'
 
324
        return SmartServerResponse((r,))
 
325
 
 
326
    def do_get(self, relpath):
 
327
        backing_file = self._backing_transport.get(relpath)
 
328
        return SmartServerResponse(('ok',), backing_file.read())
 
329
 
 
330
    def _optional_mode(self, mode):
 
331
        if mode == '':
 
332
            return None
 
333
        else:
 
334
            return int(mode)
 
335
 
 
336
    def do_append(self, relpath, mode):
 
337
        old_length = self._backing_transport.append(relpath, StringIO(self._recv_body()),
 
338
                self._optional_mode(mode))
 
339
        return SmartServerResponse(('appended', '%d' % old_length))
 
340
 
 
341
    def do_delete(self, relpath):
 
342
        self._backing_transport.delete(relpath)
 
343
 
 
344
    def do_iter_files_recursive(self, abspath):
 
345
        # XXX: the path handling needs some thought.
 
346
        #relpath = self._backing_transport.relpath(abspath)
 
347
        transport = self._backing_transport.clone(abspath)
 
348
        filenames = transport.iter_files_recursive()
 
349
        return SmartServerResponse(('names',) + tuple(filenames))
 
350
 
 
351
    def do_list_dir(self, relpath):
 
352
        filenames = self._backing_transport.list_dir(relpath)
 
353
        return SmartServerResponse(('names',) + tuple(filenames))
 
354
 
 
355
    def do_mkdir(self, relpath, mode):
 
356
        self._backing_transport.mkdir(relpath, self._optional_mode(mode))
 
357
 
 
358
    def do_move(self, rel_from, rel_to):
 
359
        self._backing_transport.move(rel_from, rel_to)
 
360
 
 
361
    def do_put(self, relpath, mode):
 
362
        self._backing_transport.put(relpath, 
 
363
                StringIO(self._recv_body()), 
 
364
                self._optional_mode(mode))
 
365
 
 
366
    def do_rename(self, rel_from, rel_to):
 
367
        self._backing_transport.rename(rel_from, rel_to)
 
368
 
 
369
    def do_rmdir(self, relpath):
 
370
        self._backing_transport.rmdir(relpath)
 
371
 
 
372
    def do_stat(self, relpath):
 
373
        stat = self._backing_transport.stat(relpath)
 
374
        return SmartServerResponse(('stat', str(stat.st_size), oct(stat.st_mode)))
 
375
        
 
376
    def do_get_bundle(self, path, revision_id):
 
377
        # open transport relative to our base
 
378
        t = self._backing_transport.clone(path)
 
379
        control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
 
380
        repo = control.open_repository()
 
381
        tmpf = tempfile.TemporaryFile()
 
382
        base_revision = revision.NULL_REVISION
 
383
        write_bundle(repo, revision_id, base_revision, tmpf)
 
384
        tmpf.seek(0)
 
385
        return SmartServerResponse((), tmpf.read())
 
386
 
 
387
    def dispatch_command(self, cmd, args):
 
388
        func = getattr(self, 'do_' + cmd, None)
 
389
        if func is None:
 
390
            raise BzrProtocolError("bad request %r" % (cmd,))
 
391
        try:
 
392
            result = func(*args)
 
393
            if result is None: 
 
394
                result = SmartServerResponse(('ok',))
 
395
            return result
 
396
        except errors.NoSuchFile, e:
 
397
            return SmartServerResponse(('NoSuchFile', e.path))
 
398
        except errors.FileExists, e:
 
399
            return SmartServerResponse(('FileExists', e.path))
 
400
        except errors.DirectoryNotEmpty, e:
 
401
            return SmartServerResponse(('DirectoryNotEmpty', e.path))
 
402
 
 
403
 
 
404
class SmartTCPServer(object):
 
405
    """Listens on a TCP socket and accepts connections from smart clients"""
 
406
 
 
407
    def __init__(self, backing_transport=None, host='127.0.0.1', port=0):
 
408
        """Construct a new server.
 
409
 
 
410
        To actually start it running, call either start_background_thread or
 
411
        serve.
 
412
 
 
413
        :param host: Name of the interface to listen on.
 
414
        :param port: TCP port to listen on, or 0 to allocate a transient port.
 
415
        """
 
416
        if backing_transport is None:
 
417
            backing_transport = memory.MemoryTransport()
 
418
        self._server_socket = socket.socket()
 
419
        self._server_socket.bind((host, port))
 
420
        self.port = self._server_socket.getsockname()[1]
 
421
        self._server_socket.listen(1)
 
422
        self._server_socket.settimeout(1)
 
423
        self.backing_transport = backing_transport
 
424
 
 
425
    def serve(self):
 
426
        # let connections timeout so that we get a chance to terminate
 
427
        # Keep a reference to the exceptions we want to catch because the socket
 
428
        # module's globals get set to None during interpreter shutdown.
 
429
        from socket import timeout as socket_timeout
 
430
        from socket import error as socket_error
 
431
        self._should_terminate = False
 
432
        while not self._should_terminate:
 
433
            try:
 
434
                self.accept_and_serve()
 
435
            except socket_timeout:
 
436
                # just check if we're asked to stop
 
437
                pass
 
438
            except socket_error, e:
 
439
                trace.warning("client disconnected: %s", e)
 
440
                pass
 
441
 
 
442
    def get_url(self):
 
443
        """Return the url of the server"""
 
444
        return "bzr://%s:%d/" % self._server_socket.getsockname()
 
445
 
 
446
    def accept_and_serve(self):
 
447
        conn, client_addr = self._server_socket.accept()
 
448
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 
449
        from_client = conn.makefile('r')
 
450
        to_client = conn.makefile('w')
 
451
        handler = SmartStreamServer(from_client, to_client,
 
452
                self.backing_transport)
 
453
        connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
 
454
        connection_thread.setDaemon(True)
 
455
        connection_thread.start()
 
456
 
 
457
    def start_background_thread(self):
 
458
        self._server_thread = threading.Thread(None,
 
459
                self.serve,
 
460
                name='server-' + self.get_url())
 
461
        self._server_thread.setDaemon(True)
 
462
        self._server_thread.start()
 
463
 
 
464
    def stop_background_thread(self):
 
465
        self._should_terminate = True
 
466
        # self._server_socket.close()
 
467
        # we used to join the thread, but it's not really necessary; it will
 
468
        # terminate in time
 
469
        ## self._server_thread.join()
 
470
 
 
471
 
 
472
class SmartTCPServer_for_testing(SmartTCPServer):
 
473
    """Server suitable for use by transport tests.
 
474
    
 
475
    This server is backed by the process's cwd.
 
476
    """
 
477
 
 
478
    def __init__(self):
 
479
        self._homedir = os.getcwd()
 
480
        # The server is set up by default like for ssh access: the client
 
481
        # passes filesystem-absolute paths; therefore the server must look
 
482
        # them up relative to the root directory.  it might be better to act
 
483
        # a public server and have the server rewrite paths into the test
 
484
        # directory.
 
485
        SmartTCPServer.__init__(self, transport.get_transport("file:///"))
 
486
        
 
487
    def setUp(self):
 
488
        """Set up server for testing"""
 
489
        self.start_background_thread()
 
490
 
 
491
    def tearDown(self):
 
492
        self.stop_background_thread()
 
493
 
 
494
    def get_url(self):
 
495
        """Return the url of the server"""
 
496
        host, port = self._server_socket.getsockname()
 
497
        # XXX: I think this is likely to break on windows -- self._homedir will
 
498
        # have backslashes (and maybe a drive letter?).
 
499
        #  -- Andrew Bennetts, 2006-08-29
 
500
        return "bzr://%s:%d%s" % (host, port, urlutils.escape(self._homedir))
 
501
 
 
502
    def get_bogus_url(self):
 
503
        """Return a URL which will fail to connect"""
 
504
        return 'bzr://127.0.0.1:1/'
 
505
 
 
506
 
 
507
class SmartStat(object):
 
508
 
 
509
    def __init__(self, size, mode):
 
510
        self.st_size = size
 
511
        self.st_mode = mode
 
512
 
 
513
 
 
514
class SmartTransport(sftp.SFTPUrlHandling):
 
515
    """Connection to a smart server.
 
516
 
 
517
    The connection holds references to pipes that can be used to send requests
 
518
    to the server.
 
519
 
 
520
    The connection has a notion of the current directory to which it's
 
521
    connected; this is incorporated in filenames passed to the server.
 
522
    
 
523
    This supports some higher-level RPC operations and can also be treated 
 
524
    like a Transport to do file-like operations.
 
525
 
 
526
    The connection can be made over a tcp socket, or (in future) an ssh pipe
 
527
    or a series of http requests.  There are concrete subclasses for each
 
528
    type: SmartTCPTransport, etc.
 
529
    """
 
530
 
 
531
    def __init__(self, server_url, clone_from=None):
 
532
        ### Technically super() here is faulty because Transport's __init__
 
533
        ### fails to take 2 parameters, and if super were to choose a silly
 
534
        ### initialisation order things would blow up. 
 
535
        super(SmartTransport, self).__init__(server_url)
 
536
        if clone_from is None:
 
537
            self._client = SmartStreamClient(self._connect_to_server)
 
538
        else:
 
539
            # credentials may be stripped from the base in some circumstances
 
540
            # as yet to be clearly defined or documented, so copy them.
 
541
            self._username = clone_from._username
 
542
            # reuse same connection
 
543
            self._client = clone_from._client
 
544
 
 
545
    def clone(self, relative_url):
 
546
        """Make a new SmartTransport related to me, sharing the same connection.
 
547
 
 
548
        This essentially opens a handle on a different remote directory.
 
549
        """
 
550
        if relative_url is None:
 
551
            return self.__class__(self.base, self)
 
552
        else:
 
553
            return self.__class__(self.abspath(relative_url), self)
 
554
 
 
555
    def is_readonly(self):
 
556
        """Smart server transport can do read/write file operations."""
 
557
        return False
 
558
                                                   
 
559
    def get_smart_client(self):
 
560
        return self._client
 
561
                                                   
 
562
    def _unparse_url(self, path):
 
563
        """Return URL for a path.
 
564
 
 
565
        :see: SFTPUrlHandling._unparse_url
 
566
        """
 
567
        # TODO: Eventually it should be possible to unify this with
 
568
        # SFTPUrlHandling._unparse_url?
 
569
        if path == '':
 
570
            path = '/'
 
571
        path = urllib.quote(path)
 
572
        netloc = urllib.quote(self._host)
 
573
        if self._username is not None:
 
574
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
575
        if self._port is not None:
 
576
            netloc = '%s:%d' % (netloc, self._port)
 
577
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
 
578
 
 
579
    def _remote_path(self, relpath):
 
580
        return self._combine_paths(self._path, relpath)
 
581
 
 
582
    def has(self, relpath):
 
583
        """Indicate whether a remote file of the given name exists or not.
 
584
 
 
585
        :see: Transport.has()
 
586
        """
 
587
        resp = self._client._call('has', self._remote_path(relpath))
 
588
        if resp == ('yes', ):
 
589
            return True
 
590
        elif resp == ('no', ):
 
591
            return False
 
592
        else:
 
593
            self._translate_error(resp)
 
594
 
 
595
    def get(self, relpath):
 
596
        """Return file-like object reading the contents of a remote file.
 
597
        
 
598
        :see: Transport.get()
 
599
        """
 
600
        remote = self._remote_path(relpath)
 
601
        resp = self._client._call('get', remote)
 
602
        if resp != ('ok', ):
 
603
            self._translate_error(resp, relpath)
 
604
        return StringIO(self._client._recv_bulk())
 
605
 
 
606
    def _optional_mode(self, mode):
 
607
        if mode is None:
 
608
            return ''
 
609
        else:
 
610
            return '%d' % mode
 
611
 
 
612
    def mkdir(self, relpath, mode=None):
 
613
        resp = self._client._call('mkdir', 
 
614
                                  self._remote_path(relpath), 
 
615
                                  self._optional_mode(mode))
 
616
        self._translate_error(resp)
 
617
 
 
618
    def put(self, relpath, upload_file, mode=None):
 
619
        # FIXME: upload_file is probably not safe for non-ascii characters -
 
620
        # should probably just pass all parameters as length-delimited
 
621
        # strings?
 
622
        # XXX: wrap strings in a StringIO.  There should be a better way of
 
623
        # handling this.
 
624
        if isinstance(upload_file, str):
 
625
            upload_file = StringIO(upload_file)
 
626
        resp = self._client._call_with_upload('put', 
 
627
                                              (self._remote_path(relpath), 
 
628
                                               self._optional_mode(mode)),
 
629
                                              upload_file.read())
 
630
        self._translate_error(resp)
 
631
 
 
632
    def append(self, relpath, from_file, mode=None):
 
633
        resp = self._client._call_with_upload('append',
 
634
                                              (self._remote_path(relpath), 
 
635
                                               self._optional_mode(mode)),
 
636
                                              from_file.read())
 
637
        if resp[0] == 'appended':
 
638
            return int(resp[1])
 
639
        self._translate_error(resp)
 
640
 
 
641
    def delete(self, relpath):
 
642
        resp = self._client._call('delete', self._remote_path(relpath))
 
643
        self._translate_error(resp)
 
644
 
 
645
    def rename(self, rel_from, rel_to):
 
646
        self._call('rename', 
 
647
                   self._remote_path(rel_from),
 
648
                   self._remote_path(rel_to))
 
649
 
 
650
    def move(self, rel_from, rel_to):
 
651
        self._call('move', 
 
652
                   self._remote_path(rel_from),
 
653
                   self._remote_path(rel_to))
 
654
 
 
655
    def rmdir(self, relpath):
 
656
        resp = self._call('rmdir', self._remote_path(relpath))
 
657
 
 
658
    def _call(self, method, *args):
 
659
        resp = self._client._call(method, *args)
 
660
        self._translate_error(resp)
 
661
 
 
662
    def _translate_error(self, resp, orig_path=None):
 
663
        """Raise an exception from a response"""
 
664
        what = resp[0]
 
665
        if what == 'ok':
 
666
            return
 
667
        elif what == 'NoSuchFile':
 
668
            if orig_path is not None:
 
669
                error_path = orig_path
 
670
            else:
 
671
                error_path = resp[1]
 
672
            raise errors.NoSuchFile(error_path)
 
673
        elif what == 'error':
 
674
            raise BzrProtocolError(unicode(resp[1]))
 
675
        elif what == 'FileExists':
 
676
            raise errors.FileExists(resp[1])
 
677
        elif what == 'DirectoryNotEmpty':
 
678
            raise errors.DirectoryNotEmpty(resp[1])
 
679
        else:
 
680
            raise BzrProtocolError('unexpected smart server error: %r' % (resp,))
 
681
 
 
682
    def _send_tuple(self, args):
 
683
        self._client._send_tuple(args)
 
684
 
 
685
    def _recv_tuple(self):
 
686
        return self._client._recv_tuple()
 
687
 
 
688
    def disconnect(self):
 
689
        self._client.disconnect()
 
690
 
 
691
    def delete_tree(self, relpath):
 
692
        raise errors.TransportNotPossible('readonly transport')
 
693
 
 
694
    def stat(self, relpath):
 
695
        resp = self._client._call('stat', self._remote_path(relpath))
 
696
        if resp[0] == 'stat':
 
697
            return SmartStat(int(resp[1]), int(resp[2], 8))
 
698
        else:
 
699
            self._translate_error(resp)
 
700
 
 
701
    ## def lock_read(self, relpath):
 
702
    ##     """Lock the given file for shared (read) access.
 
703
    ##     :return: A lock object, which should be passed to Transport.unlock()
 
704
    ##     """
 
705
    ##     # The old RemoteBranch ignore lock for reading, so we will
 
706
    ##     # continue that tradition and return a bogus lock object.
 
707
    ##     class BogusLock(object):
 
708
    ##         def __init__(self, path):
 
709
    ##             self.path = path
 
710
    ##         def unlock(self):
 
711
    ##             pass
 
712
    ##     return BogusLock(relpath)
 
713
 
 
714
    def listable(self):
 
715
        return True
 
716
 
 
717
    def list_dir(self, relpath):
 
718
        resp = self._client._call('list_dir',
 
719
                                  self._remote_path(relpath))
 
720
        if resp[0] == 'names':
 
721
            return [name.encode('ascii') for name in resp[1:]]
 
722
        else:
 
723
            self._translate_error(resp)
 
724
 
 
725
    def iter_files_recursive(self):
 
726
        resp = self._client._call('iter_files_recursive',
 
727
                                  self._remote_path(''))
 
728
        if resp[0] == 'names':
 
729
            return resp[1:]
 
730
        else:
 
731
            self._translate_error(resp)
 
732
 
 
733
 
 
734
class SmartStreamClient(SmartProtocolBase):
 
735
    """Connection to smart server over two streams"""
 
736
 
 
737
    def __init__(self, connect_func):
 
738
        self._connect_func = connect_func
 
739
        self._connected = False
 
740
 
 
741
    def __del__(self):
 
742
        self.disconnect()
 
743
 
 
744
    def _ensure_connection(self):
 
745
        if not self._connected:
 
746
            self._in, self._out = self._connect_func()
 
747
            self._connected = True
 
748
 
 
749
    def _send_tuple(self, args):
 
750
        self._ensure_connection()
 
751
        _send_tuple(self._out, args)
 
752
 
 
753
    def _send_bulk_data(self, body):
 
754
        self._ensure_connection()
 
755
        SmartProtocolBase._send_bulk_data(self, body)
 
756
        
 
757
    def _recv_bulk(self):
 
758
        self._ensure_connection()
 
759
        return SmartProtocolBase._recv_bulk(self)
 
760
 
 
761
    def _recv_tuple(self):
 
762
        self._ensure_connection()
 
763
        return SmartProtocolBase._recv_tuple(self)
 
764
 
 
765
    def _recv_trailer(self):
 
766
        self._ensure_connection()
 
767
        return SmartProtocolBase._recv_trailer(self)
 
768
 
 
769
    def disconnect(self):
 
770
        """Close connection to the server"""
 
771
        if self._connected:
 
772
            self._out.close()
 
773
            self._in.close()
 
774
 
 
775
    def _call(self, *args):
 
776
        self._send_tuple(args)
 
777
        return self._recv_tuple()
 
778
 
 
779
    def _call_with_upload(self, method, args, body):
 
780
        """Call an rpc, supplying bulk upload data.
 
781
 
 
782
        :param method: method name to call
 
783
        :param args: parameter args tuple
 
784
        :param body: upload body as a byte string
 
785
        """
 
786
        self._send_tuple((method,) + args)
 
787
        self._send_bulk_data(body)
 
788
        return self._recv_tuple()
 
789
 
 
790
    def query_version(self):
 
791
        """Return protocol version number of the server."""
 
792
        # XXX: should make sure it's empty
 
793
        self._send_tuple(('hello',))
 
794
        resp = self._recv_tuple()
 
795
        if resp == ('ok', '1'):
 
796
            return 1
 
797
        else:
 
798
            raise BzrProtocolError("bad response %r" % (resp,))
 
799
 
 
800
 
 
801
 
 
802
class SmartTCPTransport(SmartTransport):
 
803
    """Connection to smart server over plain tcp"""
 
804
 
 
805
    def __init__(self, url, clone_from=None):
 
806
        super(SmartTCPTransport, self).__init__(url, clone_from)
 
807
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
 
808
                transport.split_url(url)
 
809
        try:
 
810
            self._port = int(self._port)
 
811
        except (ValueError, TypeError), e:
 
812
            raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
 
813
        self._socket = None
 
814
 
 
815
    def _connect_to_server(self):
 
816
        self._socket = socket.socket()
 
817
        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 
818
        result = self._socket.connect_ex((self._host, int(self._port)))
 
819
        if result:
 
820
            raise errors.ConnectionError("failed to connect to %s:%d: %s" %
 
821
                    (self._host, self._port, os.strerror(result)))
 
822
        # TODO: May be more efficient to just treat them as sockets
 
823
        # throughout?  But what about pipes to ssh?...
 
824
        to_server = self._socket.makefile('w')
 
825
        from_server = self._socket.makefile('r')
 
826
        return from_server, to_server
 
827
 
 
828
    def disconnect(self):
 
829
        super(SmartTCPTransport, self).disconnect()
 
830
        # XXX: Is closing the socket as well as closing the files really
 
831
        # necessary?
 
832
        if self._socket is not None:
 
833
            self._socket.close()
 
834
 
 
835
 
 
836
class SmartSSHTransport(SmartTransport):
 
837
    """Connection to smart server over SSH."""
 
838
 
 
839
    def __init__(self, url, clone_from=None):
 
840
        # TODO: all this probably belongs in the parent class.
 
841
        super(SmartSSHTransport, self).__init__(url, clone_from)
 
842
        self._scheme, self._username, self._password, self._host, self._port, self._path = \
 
843
                transport.split_url(url)
 
844
        try:
 
845
            if self._port is not None:
 
846
                self._port = int(self._port)
 
847
        except (ValueError, TypeError), e:
 
848
            raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
 
849
 
 
850
    def _connect_to_server(self):
 
851
        # XXX: don't hardcode vendor
 
852
        # XXX: cannot pass password to SSHSubprocess yet
 
853
        if self._password is not None:
 
854
            raise errors.InvalidURL("SSH smart transport doesn't handle passwords")
 
855
        self._ssh_connection = sftp.SSHSubprocess(self._host, 'openssh',
 
856
                port=self._port, user=self._username,
 
857
                command=['bzr', 'serve', '--inet'])
 
858
        return self._ssh_connection.get_filelike_channels()
 
859
 
 
860
    def disconnect(self):
 
861
        super(SmartSSHTransport, self).disconnect()
 
862
        self._ssh_connection.close()
 
863
 
 
864
 
 
865
def get_test_permutations():
 
866
    """Return (transport, server) permutations for testing"""
 
867
    return [(SmartTCPTransport, SmartTCPServer_for_testing)]