33
from bzrlib.config import config_dir, ensure_config_dir_exists
34
from bzrlib.errors import (ConnectionError,
32
from bzrlib.errors import (FileExists,
36
33
TransportNotPossible, NoSuchFile, PathNotChild,
36
from bzrlib.config import config_dir, ensure_config_dir_exists
37
from bzrlib.trace import mutter, warning, error
38
from bzrlib.transport import Transport, register_transport
42
39
from bzrlib.osutils import pathjoin, fancy_rename
43
from bzrlib.trace import mutter, warning, error
44
from bzrlib.transport import (
45
register_urlparse_netloc_protocol,
54
except ImportError, e:
55
raise ParamikoNotPresent(e)
45
error('The SFTP transport requires paramiko.')
57
48
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
58
49
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
61
52
from paramiko.sftp_file import SFTPFile
62
53
from paramiko.sftp_client import SFTPClient
65
register_urlparse_netloc_protocol('sftp')
69
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
70
# doesn't handle it itself.
71
# <https://launchpad.net/products/bzr/+bug/41433/+index>
73
signal.signal(signal.SIGINT, signal.SIG_IGN)
76
def os_specific_subprocess_params():
77
"""Get O/S specific subprocess parameters."""
78
if sys.platform == 'win32':
79
# setting the process group and closing fds is not supported on
83
# We close fds other than the pipes as the child process does not need
86
# We also set the child process to ignore SIGINT. Normally the signal
87
# would be sent to every process in the foreground process group, but
88
# this causes it to be seen only by bzr and not by ssh. Python will
89
# generate a KeyboardInterrupt in bzr, and we will then have a chance
90
# to release locks or do other cleanup over ssh before the connection
92
# <https://launchpad.net/products/bzr/+bug/5987>
94
# Running it in a separate process group is not good because then it
95
# can't get non-echoed input of a password or passphrase.
96
# <https://launchpad.net/products/bzr/+bug/40508>
97
return {'preexec_fn': _ignore_sigint,
102
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
103
_default_do_prefetch = False
104
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
105
_default_do_prefetch = True
55
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
59
if sys.platform == 'win32':
60
# close_fds not supported on win32
108
63
_ssh_vendor = None
109
64
def _get_ssh_vendor():
389
305
extra = ': ' + ', '.join(error)
390
306
raise PathNotChild(abspath, self.base, extra=extra)
391
307
pl = len(self._path)
392
return path[pl:].strip('/')
308
return path[pl:].lstrip('/')
394
310
def has(self, relpath):
396
312
Does the target location exist?
399
self._sftp.stat(self._remote_path(relpath))
315
self._sftp.stat(self._abspath(relpath))
404
def get(self, relpath):
320
def get(self, relpath, decode=False):
406
322
Get the file at the given relative path.
408
324
:param relpath: The relative path to the file
411
path = self._remote_path(relpath)
327
path = self._abspath(relpath)
412
328
f = self._sftp.file(path, mode='rb')
413
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
329
if self._do_prefetch and hasattr(f, 'prefetch'):
416
332
except (IOError, paramiko.SSHException), e:
539
452
mutter('Raising exception with errno %s', e.errno)
542
def append(self, relpath, f, mode=None):
455
def append(self, relpath, f):
544
457
Append the text in the file-like object into the final
548
path = self._remote_path(relpath)
461
path = self._abspath(relpath)
549
462
fout = self._sftp.file(path, 'ab')
551
self._sftp.chmod(path, mode)
553
463
self._pump(f, fout)
555
464
except (IOError, paramiko.SSHException), e:
556
465
self._translate_io_exception(e, relpath, ': unable to append')
558
def rename(self, rel_from, rel_to):
559
"""Rename without special overwriting"""
467
def copy(self, rel_from, rel_to):
468
"""Copy the item at rel_from to the location at rel_to"""
469
path_from = self._abspath(rel_from)
470
path_to = self._abspath(rel_to)
471
self._copy_abspaths(path_from, path_to)
473
def _copy_abspaths(self, path_from, path_to, mode=None):
474
"""Copy files given an absolute path
476
:param path_from: Path on remote server to read
477
:param path_to: Path on remote server to write
480
TODO: Should the destination location be atomically created?
481
This has not been specified
482
TODO: This should use some sort of remote copy, rather than
483
pulling the data locally, and then writing it remotely
561
self._sftp.rename(self._remote_path(rel_from),
562
self._remote_path(rel_to))
486
fin = self._sftp.file(path_from, 'rb')
488
self._put(path_to, fin, mode=mode)
563
491
except (IOError, paramiko.SSHException), e:
564
self._translate_io_exception(e, rel_from,
565
': unable to rename to %r' % (rel_to))
567
def _rename_and_overwrite(self, abs_from, abs_to):
492
self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
494
def copy_to(self, relpaths, other, mode=None, pb=None):
495
"""Copy a set of entries from self into another Transport.
497
:param relpaths: A list/generator of entries to be copied.
499
if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
500
# Both from & to are on the same remote filesystem
501
# We can use a remote copy, instead of pulling locally, and pushing
503
total = self._get_total(relpaths)
505
for path in relpaths:
506
path_from = self._abspath(relpath)
507
path_to = other._abspath(relpath)
508
self._update_pb(pb, 'copy-to', count, total)
509
self._copy_abspaths(path_from, path_to, mode=mode)
513
return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
515
def _rename(self, abs_from, abs_to):
568
516
"""Do a fancy rename on the remote server.
570
518
Using the implementation provided by osutils.
599
547
Return a list of all files at the given location.
601
549
# does anything actually use this?
602
path = self._remote_path(relpath)
550
path = self._abspath(relpath)
604
552
return self._sftp.listdir(path)
605
553
except (IOError, paramiko.SSHException), e:
606
554
self._translate_io_exception(e, path, ': failed to list_dir')
608
def rmdir(self, relpath):
609
"""See Transport.rmdir."""
610
path = self._remote_path(relpath)
612
return self._sftp.rmdir(path)
613
except (IOError, paramiko.SSHException), e:
614
self._translate_io_exception(e, path, ': failed to rmdir')
616
556
def stat(self, relpath):
617
557
"""Return the stat information for a file."""
618
path = self._remote_path(relpath)
558
path = self._abspath(relpath)
620
560
return self._sftp.stat(path)
621
561
except (IOError, paramiko.SSHException), e:
881
815
self._translate_io_exception(e, abspath, ': unable to open',
882
816
failure_exc=FileExists)
885
# ------------- server test implementation --------------
889
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
891
STUB_SERVER_KEY = """
892
-----BEGIN RSA PRIVATE KEY-----
893
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
894
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
895
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
896
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
897
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
898
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
899
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
900
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
901
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
902
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
903
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
904
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
905
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
906
-----END RSA PRIVATE KEY-----
910
class SingleListener(threading.Thread):
912
def __init__(self, callback):
913
threading.Thread.__init__(self)
914
self._callback = callback
915
self._socket = socket.socket()
916
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
917
self._socket.bind(('localhost', 0))
918
self._socket.listen(1)
919
self.port = self._socket.getsockname()[1]
920
self.stop_event = threading.Event()
923
s, _ = self._socket.accept()
924
# now close the listen socket
927
self._callback(s, self.stop_event)
929
pass #Ignore socket errors
931
# probably a failed test
932
warning('Exception from within unit test server thread: %r' % x)
935
self.stop_event.set()
936
# use a timeout here, because if the test fails, the server thread may
937
# never notice the stop_event.
941
class SFTPServer(Server):
942
"""Common code for SFTP server facilities."""
945
self._original_vendor = None
947
self._server_homedir = None
948
self._listener = None
950
self._vendor = 'none'
954
def _get_sftp_url(self, path):
955
"""Calculate an sftp url to this server for path."""
956
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
958
def log(self, message):
959
"""StubServer uses this to log when a new server is created."""
960
self.logs.append(message)
962
def _run_server(self, s, stop_event):
963
ssh_server = paramiko.Transport(s)
964
key_file = os.path.join(self._homedir, 'test_rsa.key')
965
file(key_file, 'w').write(STUB_SERVER_KEY)
966
host_key = paramiko.RSAKey.from_private_key_file(key_file)
967
ssh_server.add_server_key(host_key)
968
server = StubServer(self)
969
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
970
StubSFTPServer, root=self._root,
971
home=self._server_homedir)
972
event = threading.Event()
973
ssh_server.start_server(event, server)
975
stop_event.wait(30.0)
979
self._original_vendor = _ssh_vendor
980
_ssh_vendor = self._vendor
981
self._homedir = os.getcwdu()
982
if self._server_homedir is None:
983
self._server_homedir = self._homedir
985
# FIXME WINDOWS: _root should be _server_homedir[0]:/
986
self._listener = SingleListener(self._run_server)
987
self._listener.setDaemon(True)
988
self._listener.start()
991
"""See bzrlib.transport.Server.tearDown."""
993
self._listener.stop()
994
_ssh_vendor = self._original_vendor
997
class SFTPFullAbsoluteServer(SFTPServer):
998
"""A test server for sftp transports, using absolute urls and ssh."""
1001
"""See bzrlib.transport.Server.get_url."""
1002
return self._get_sftp_url(urlescape(self._homedir[1:]))
1005
class SFTPServerWithoutSSH(SFTPServer):
1006
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
1009
super(SFTPServerWithoutSSH, self).__init__()
1010
self._vendor = 'loopback'
1012
def _run_server(self, sock, stop_event):
1013
class FakeChannel(object):
1014
def get_transport(self):
1016
def get_log_channel(self):
1020
def get_hexdump(self):
1023
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1024
root=self._root, home=self._server_homedir)
1025
server.start_subsystem('sftp', None, sock)
1026
server.finish_subsystem()
1029
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1030
"""A test server for sftp transports, using absolute urls."""
1033
"""See bzrlib.transport.Server.get_url."""
1034
return self._get_sftp_url(urlescape(self._homedir[1:]))
1037
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1038
"""A test server for sftp transports, using homedir relative urls."""
1041
"""See bzrlib.transport.Server.get_url."""
1042
return self._get_sftp_url("~/")
1045
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1046
"""A test servere for sftp transports, using absolute urls to non-home."""
1049
self._server_homedir = '/dev/noone/runs/tests/here'
1050
super(SFTPSiblingAbsoluteServer, self).setUp()
1053
def get_test_permutations():
1054
"""Return the permutations to be used in testing."""
1055
return [(SFTPTransport, SFTPAbsoluteServer),
1056
(SFTPTransport, SFTPHomeDirServer),
1057
(SFTPTransport, SFTPSiblingAbsoluteServer),