/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

Remove a little bit of duplication in ssh.py

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