~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Martin Pool
  • Date: 2006-01-12 06:37:23 UTC
  • mfrom: (1534.1.6 integration)
  • Revision ID: mbp@sourcefrog.net-20060112063723-4ec91b5ff30f0830
[merge] robertc-integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import subprocess
30
30
import weakref
31
31
 
32
 
from bzrlib.errors import (FileExists, 
 
32
from bzrlib.config import config_dir, ensure_config_dir_exists
 
33
from bzrlib.errors import (ConnectionError,
 
34
                           FileExists, 
33
35
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
36
                           TransportError,
35
 
                           LockError)
36
 
from bzrlib.config import config_dir, ensure_config_dir_exists
 
37
                           LockError
 
38
                           )
 
39
from bzrlib.osutils import pathjoin, fancy_rename
37
40
from bzrlib.trace import mutter, warning, error
38
 
from bzrlib.transport import Transport, register_transport
39
 
from bzrlib.osutils import pathjoin, fancy_rename
 
41
from bzrlib.transport import Transport, Server, urlescape
40
42
import bzrlib.ui
41
43
 
42
44
try:
99
101
 
100
102
class SFTPSubprocess:
101
103
    """A socket-like object that talks to an ssh subprocess via pipes."""
102
 
    def __init__(self, hostname, port=None, user=None):
103
 
        vendor = _get_ssh_vendor()
 
104
    def __init__(self, hostname, vendor, port=None, user=None):
104
105
        assert vendor in ['openssh', 'ssh']
105
106
        if vendor == 'openssh':
106
107
            args = ['ssh',
199
200
        self.lock_path = path + '.write-lock'
200
201
        self.transport = transport
201
202
        try:
202
 
            abspath = transport._abspath(self.lock_path)
 
203
            # RBC 20060103 FIXME should we be using private methods here ?
 
204
            abspath = transport._remote_path(self.lock_path)
203
205
            self.lock_file = transport._sftp_open_exclusive(abspath)
204
206
        except FileExists:
205
207
            raise LockError('File %r already locked' % (self.path,))
221
223
            # What specific errors should we catch here?
222
224
            pass
223
225
 
 
226
 
 
227
 
224
228
class SFTPTransport (Transport):
225
229
    """
226
230
    Transport implementation for SFTP access.
231
235
        assert base.startswith('sftp://')
232
236
        self._parse_url(base)
233
237
        base = self._unparse_url()
 
238
        if base[-1] != '/':
 
239
            base = base + '/'
234
240
        super(SFTPTransport, self).__init__(base)
235
241
        if clone_from is None:
236
242
            self._sftp_connect()
263
269
        @param relpath: the relative path or path components
264
270
        @type relpath: str or list
265
271
        """
266
 
        return self._unparse_url(self._abspath(relpath))
 
272
        return self._unparse_url(self._remote_path(relpath))
267
273
    
268
 
    def _abspath(self, relpath):
269
 
        """Return the absolute path segment without the SFTP URL."""
 
274
    def _remote_path(self, relpath):
 
275
        """Return the path to be passed along the sftp protocol for relpath.
 
276
        
 
277
        relpath is a urlencoded string.
 
278
        """
270
279
        # FIXME: share the common code across transports
271
280
        assert isinstance(relpath, basestring)
272
 
        relpath = [urllib.unquote(relpath)]
 
281
        relpath = urllib.unquote(relpath).split('/')
273
282
        basepath = self._path.split('/')
274
283
        if len(basepath) > 0 and basepath[-1] == '':
275
284
            basepath = basepath[:-1]
287
296
                basepath.append(p)
288
297
 
289
298
        path = '/'.join(basepath)
290
 
        # could still be a "relative" path here, but relative on the sftp server
291
299
        return path
292
300
 
293
301
    def relpath(self, abspath):
305
313
            extra = ': ' + ', '.join(error)
306
314
            raise PathNotChild(abspath, self.base, extra=extra)
307
315
        pl = len(self._path)
308
 
        return path[pl:].lstrip('/')
 
316
        return path[pl:].strip('/')
309
317
 
310
318
    def has(self, relpath):
311
319
        """
312
320
        Does the target location exist?
313
321
        """
314
322
        try:
315
 
            self._sftp.stat(self._abspath(relpath))
 
323
            self._sftp.stat(self._remote_path(relpath))
316
324
            return True
317
325
        except IOError:
318
326
            return False
324
332
        :param relpath: The relative path to the file
325
333
        """
326
334
        try:
327
 
            path = self._abspath(relpath)
 
335
            path = self._remote_path(relpath)
328
336
            f = self._sftp.file(path, mode='rb')
329
337
            if self._do_prefetch and hasattr(f, 'prefetch'):
330
338
                f.prefetch()
359
367
        :param f:       File-like or string object.
360
368
        :param mode: The final mode for the file
361
369
        """
362
 
        final_path = self._abspath(relpath)
 
370
        final_path = self._remote_path(relpath)
363
371
        self._put(final_path, f, mode=mode)
364
372
 
365
373
    def _put(self, abspath, f, mode=None):
412
420
    def mkdir(self, relpath, mode=None):
413
421
        """Create a directory at the given path."""
414
422
        try:
415
 
            path = self._abspath(relpath)
 
423
            path = self._remote_path(relpath)
416
424
            # In the paramiko documentation, it says that passing a mode flag 
417
425
            # will filtered against the server umask.
418
426
            # StubSFTPServer does not do this, which would be nice, because it is
460
468
        location.
461
469
        """
462
470
        try:
463
 
            path = self._abspath(relpath)
 
471
            path = self._remote_path(relpath)
464
472
            fout = self._sftp.file(path, 'ab')
465
473
            self._pump(f, fout)
466
474
        except (IOError, paramiko.SSHException), e:
468
476
 
469
477
    def copy(self, rel_from, rel_to):
470
478
        """Copy the item at rel_from to the location at rel_to"""
471
 
        path_from = self._abspath(rel_from)
472
 
        path_to = self._abspath(rel_to)
 
479
        path_from = self._remote_path(rel_from)
 
480
        path_to = self._remote_path(rel_to)
473
481
        self._copy_abspaths(path_from, path_to)
474
482
 
475
483
    def _copy_abspaths(self, path_from, path_to, mode=None):
505
513
            total = self._get_total(relpaths)
506
514
            count = 0
507
515
            for path in relpaths:
508
 
                path_from = self._abspath(relpath)
509
 
                path_to = other._abspath(relpath)
 
516
                path_from = self._remote_path(relpath)
 
517
                path_to = other._remote_path(relpath)
510
518
                self._update_pb(pb, 'copy-to', count, total)
511
519
                self._copy_abspaths(path_from, path_to, mode=mode)
512
520
                count += 1
528
536
 
529
537
    def move(self, rel_from, rel_to):
530
538
        """Move the item at rel_from to the location at rel_to"""
531
 
        path_from = self._abspath(rel_from)
532
 
        path_to = self._abspath(rel_to)
 
539
        path_from = self._remote_path(rel_from)
 
540
        path_to = self._remote_path(rel_to)
533
541
        self._rename(path_from, path_to)
534
542
 
535
543
    def delete(self, relpath):
536
544
        """Delete the item at relpath"""
537
 
        path = self._abspath(relpath)
 
545
        path = self._remote_path(relpath)
538
546
        try:
539
547
            self._sftp.remove(path)
540
548
        except (IOError, paramiko.SSHException), e:
549
557
        Return a list of all files at the given location.
550
558
        """
551
559
        # does anything actually use this?
552
 
        path = self._abspath(relpath)
 
560
        path = self._remote_path(relpath)
553
561
        try:
554
562
            return self._sftp.listdir(path)
555
563
        except (IOError, paramiko.SSHException), e:
557
565
 
558
566
    def stat(self, relpath):
559
567
        """Return the stat information for a file."""
560
 
        path = self._abspath(relpath)
 
568
        path = self._remote_path(relpath)
561
569
        try:
562
570
            return self._sftp.stat(path)
563
571
        except (IOError, paramiko.SSHException), e:
664
672
        
665
673
        vendor = _get_ssh_vendor()
666
674
        if vendor != 'none':
667
 
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
675
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
676
                                  self._username)
668
677
            self._sftp = SFTPClient(sock)
669
678
        else:
670
679
            self._paramiko_connect()
744
753
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
745
754
            return
746
755
 
747
 
 
748
756
        if self._password:
749
757
            try:
750
758
                transport.auth_password(username, self._password)
817
825
            self._translate_io_exception(e, abspath, ': unable to open',
818
826
                failure_exc=FileExists)
819
827
 
 
828
 
 
829
# ------------- server test implementation --------------
 
830
import socket
 
831
import threading
 
832
 
 
833
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
 
834
 
 
835
STUB_SERVER_KEY = """
 
836
-----BEGIN RSA PRIVATE KEY-----
 
837
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
838
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
839
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
840
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
841
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
842
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
843
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
844
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
845
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
846
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
847
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
848
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
849
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
850
-----END RSA PRIVATE KEY-----
 
851
"""
 
852
    
 
853
 
 
854
class SingleListener(threading.Thread):
 
855
 
 
856
    def __init__(self, callback):
 
857
        threading.Thread.__init__(self)
 
858
        self._callback = callback
 
859
        self._socket = socket.socket()
 
860
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
861
        self._socket.bind(('localhost', 0))
 
862
        self._socket.listen(1)
 
863
        self.port = self._socket.getsockname()[1]
 
864
        self.stop_event = threading.Event()
 
865
 
 
866
    def run(self):
 
867
        s, _ = self._socket.accept()
 
868
        # now close the listen socket
 
869
        self._socket.close()
 
870
        self._callback(s, self.stop_event)
 
871
    
 
872
    def stop(self):
 
873
        self.stop_event.set()
 
874
        # We should consider waiting for the other thread
 
875
        # to stop, because otherwise we get spurious
 
876
        #   bzr: ERROR: Socket exception: Connection reset by peer (54)
 
877
        # because the test suite finishes before the thread has a chance
 
878
        # to close. (Especially when only running a few tests)
 
879
        
 
880
        
 
881
class SFTPServer(Server):
 
882
    """Common code for SFTP server facilities."""
 
883
 
 
884
    def _get_sftp_url(self, path):
 
885
        """Calculate a sftp url to this server for path."""
 
886
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
 
887
 
 
888
    def __init__(self):
 
889
        self._original_vendor = None
 
890
        self._homedir = None
 
891
        self._server_homedir = None
 
892
        self._listener = None
 
893
        self._root = None
 
894
        # sftp server logs
 
895
        self.logs = []
 
896
 
 
897
    def log(self, message):
 
898
        """What to do here? do we need this? Its for the StubServer.."""
 
899
        self.logs.append(message)
 
900
 
 
901
    def _run_server(self, s, stop_event):
 
902
        ssh_server = paramiko.Transport(s)
 
903
        key_file = os.path.join(self._homedir, 'test_rsa.key')
 
904
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
905
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
906
        ssh_server.add_server_key(host_key)
 
907
        server = StubServer(self)
 
908
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
909
                                         StubSFTPServer, root=self._root,
 
910
                                         home=self._server_homedir)
 
911
        event = threading.Event()
 
912
        ssh_server.start_server(event, server)
 
913
        event.wait(5.0)
 
914
        stop_event.wait(30.0)
 
915
 
 
916
    def setUp(self):
 
917
        """See bzrlib.transport.Server.setUp."""
 
918
        # XXX: 20051124 jamesh
 
919
        # The tests currently pop up a password prompt when an external ssh
 
920
        # is used.  This forces the use of the paramiko implementation.
 
921
        global _ssh_vendor
 
922
        self._original_vendor = _ssh_vendor
 
923
        _ssh_vendor = 'none'
 
924
        self._homedir = os.getcwdu()
 
925
        if self._server_homedir is None:
 
926
            self._server_homedir = self._homedir
 
927
        self._root = '/'
 
928
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
 
929
        self._listener = SingleListener(self._run_server)
 
930
        self._listener.setDaemon(True)
 
931
        self._listener.start()
 
932
 
 
933
    def tearDown(self):
 
934
        """See bzrlib.transport.Server.tearDown."""
 
935
        global _ssh_vendor
 
936
        self._listener.stop()
 
937
        _ssh_vendor = self._original_vendor
 
938
 
 
939
 
 
940
class SFTPAbsoluteServer(SFTPServer):
 
941
    """A test server for sftp transports, using absolute urls."""
 
942
 
 
943
    def get_url(self):
 
944
        """See bzrlib.transport.Server.get_url."""
 
945
        return self._get_sftp_url("%%2f%s" % 
 
946
                urlescape(self._homedir[1:]))
 
947
 
 
948
 
 
949
class SFTPHomeDirServer(SFTPServer):
 
950
    """A test server for sftp transports, using homedir relative urls."""
 
951
 
 
952
    def get_url(self):
 
953
        """See bzrlib.transport.Server.get_url."""
 
954
        return self._get_sftp_url("")
 
955
 
 
956
 
 
957
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
958
    """A test servere for sftp transports, using absolute urls to non-home."""
 
959
 
 
960
    def setUp(self):
 
961
        self._server_homedir = '/dev/noone/runs/tests/here'
 
962
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
963
 
 
964
 
 
965
def get_test_permutations():
 
966
    """Return the permutations to be used in testing."""
 
967
    return [(SFTPTransport, SFTPAbsoluteServer),
 
968
            (SFTPTransport, SFTPHomeDirServer),
 
969
            (SFTPTransport, SFTPSiblingAbsoluteServer),
 
970
            ]