62
51
from paramiko.sftp_file import SFTPFile
63
52
from paramiko.sftp_client import SFTPClient
66
register_urlparse_netloc_protocol('sftp')
70
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
71
# doesn't handle it itself.
72
# <https://launchpad.net/products/bzr/+bug/41433/+index>
74
signal.signal(signal.SIGINT, signal.SIG_IGN)
77
def os_specific_subprocess_params():
78
"""Get O/S specific subprocess parameters."""
79
if sys.platform == 'win32':
80
# setting the process group and closing fds is not supported on
84
# We close fds other than the pipes as the child process does not need
87
# We also set the child process to ignore SIGINT. Normally the signal
88
# would be sent to every process in the foreground process group, but
89
# this causes it to be seen only by bzr and not by ssh. Python will
90
# generate a KeyboardInterrupt in bzr, and we will then have a chance
91
# to release locks or do other cleanup over ssh before the connection
93
# <https://launchpad.net/products/bzr/+bug/5987>
95
# Running it in a separate process group is not good because then it
96
# can't get non-echoed input of a password or passphrase.
97
# <https://launchpad.net/products/bzr/+bug/40508>
98
return {'preexec_fn': _ignore_sigint,
103
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
104
_default_do_prefetch = False
105
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
106
_default_do_prefetch = True
54
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
58
if sys.platform == 'win32':
59
# close_fds not supported on win32
109
62
_ssh_vendor = None
110
63
def _get_ssh_vendor():
439
def put(self, relpath, f, mode=None):
345
def put(self, relpath, f):
441
347
Copy the file-like or string object into the location.
443
349
:param relpath: Location to put the contents, relative to base.
444
350
:param f: File-like or string object.
445
:param mode: The final mode for the file
447
final_path = self._remote_path(relpath)
448
self._put(final_path, f, mode=mode)
450
def _put(self, abspath, f, mode=None):
451
"""Helper function so both put() and copy_abspaths can reuse the code"""
452
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
352
final_path = self._abspath(relpath)
353
tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
453
354
os.getpid(), random.randint(0,0x7FFFFFFF))
454
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
355
tmp_abspath = self._abspath(tmp_relpath)
356
fout = self._sftp_open_exclusive(tmp_relpath)
458
fout.set_pipelined(True)
459
360
self._pump(f, fout)
460
except (IOError, paramiko.SSHException), e:
461
self._translate_io_exception(e, tmp_abspath)
463
self._sftp.chmod(tmp_abspath, mode)
466
self._rename_and_overwrite(tmp_abspath, abspath)
361
except (paramiko.SSHException, IOError), e:
362
self._translate_io_exception(e, relpath, ': unable to write')
467
363
except Exception, e:
468
364
# If we fail, try to clean up the temporary file
469
365
# before we throw the exception
470
366
# but don't let another exception mess things up
471
# Write out the traceback, because otherwise
472
# the catch and throw destroys it
474
mutter(traceback.format_exc())
478
369
self._sftp.remove(tmp_abspath)
480
# raise the saved except
482
# raise the original with its traceback if we can.
374
# sftp rename doesn't allow overwriting, so play tricks:
375
tmp_safety = 'bzr.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0, 0x7FFFFFFF))
376
tmp_safety = self._abspath(tmp_safety)
378
self._sftp.rename(final_path, tmp_safety)
385
self._sftp.rename(tmp_abspath, final_path)
386
except (paramiko.SSHException, IOError), e:
387
self._translate_io_exception(e, relpath, ': unable to rename')
393
self._sftp.unlink(tmp_safety)
395
self._sftp.rename(tmp_safety, final_path)
485
397
def iter_files_recursive(self):
486
398
"""Walk the relative paths of all files in this transport."""
497
def mkdir(self, relpath, mode=None):
409
def mkdir(self, relpath):
498
410
"""Create a directory at the given path."""
500
path = self._remote_path(relpath)
501
# In the paramiko documentation, it says that passing a mode flag
502
# will filtered against the server umask.
503
# StubSFTPServer does not do this, which would be nice, because it is
504
# what we really want :)
505
# However, real servers do use umask, so we really should do it that way
412
path = self._abspath(relpath)
506
413
self._sftp.mkdir(path)
508
self._sftp.chmod(path, mode=mode)
509
414
except (paramiko.SSHException, IOError), e:
510
self._translate_io_exception(e, path, ': unable to mkdir',
415
self._translate_io_exception(e, relpath, ': unable to mkdir',
511
416
failure_exc=FileExists)
513
def _translate_io_exception(self, e, path, more_info='',
514
failure_exc=PathError):
418
def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
515
419
"""Translate a paramiko or IOError into a friendlier exception.
517
421
:param e: The original exception
535
439
# strange but true, for the paramiko server.
536
440
if (e.args == ('Failure',)):
537
441
raise failure_exc(path, str(e) + more_info)
538
mutter('Raising exception with args %s', e.args)
539
if hasattr(e, 'errno'):
540
mutter('Raising exception with errno %s', e.errno)
543
def append(self, relpath, f, mode=None):
444
def append(self, relpath, f):
545
446
Append the text in the file-like object into the final
549
path = self._remote_path(relpath)
450
path = self._abspath(relpath)
550
451
fout = self._sftp.file(path, 'ab')
552
self._sftp.chmod(path, mode)
554
452
self._pump(f, fout)
556
453
except (IOError, paramiko.SSHException), e:
557
454
self._translate_io_exception(e, relpath, ': unable to append')
559
def rename(self, rel_from, rel_to):
560
"""Rename without special overwriting"""
562
self._sftp.rename(self._remote_path(rel_from),
563
self._remote_path(rel_to))
564
except (IOError, paramiko.SSHException), e:
565
self._translate_io_exception(e, rel_from,
566
': unable to rename to %r' % (rel_to))
568
def _rename_and_overwrite(self, abs_from, abs_to):
569
"""Do a fancy rename on the remote server.
571
Using the implementation provided by osutils.
574
fancy_rename(abs_from, abs_to,
575
rename_func=self._sftp.rename,
576
unlink_func=self._sftp.remove)
577
except (IOError, paramiko.SSHException), e:
578
self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
456
def copy(self, rel_from, rel_to):
457
"""Copy the item at rel_from to the location at rel_to"""
458
path_from = self._abspath(rel_from)
459
path_to = self._abspath(rel_to)
460
self._copy_abspaths(path_from, path_to)
462
def _copy_abspaths(self, path_from, path_to):
463
"""Copy files given an absolute path
465
:param path_from: Path on remote server to read
466
:param path_to: Path on remote server to write
469
TODO: Should the destination location be atomically created?
470
This has not been specified
471
TODO: This should use some sort of remote copy, rather than
472
pulling the data locally, and then writing it remotely
475
fin = self._sftp.file(path_from, 'rb')
477
fout = self._sftp.file(path_to, 'wb')
479
fout.set_pipelined(True)
480
self._pump(fin, fout)
485
except (IOError, paramiko.SSHException), e:
486
self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
488
def copy_to(self, relpaths, other, pb=None):
489
"""Copy a set of entries from self into another Transport.
491
:param relpaths: A list/generator of entries to be copied.
493
if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
494
# Both from & to are on the same remote filesystem
495
# We can use a remote copy, instead of pulling locally, and pushing
497
total = self._get_total(relpaths)
499
for path in relpaths:
500
path_from = self._abspath(relpath)
501
path_to = other._abspath(relpath)
502
self._update_pb(pb, 'copy-to', count, total)
503
self._copy_abspaths(path_from, path_to)
507
return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
509
# The dummy implementation just does a simple get + put
510
def copy_entry(path):
511
other.put(path, self.get(path))
513
return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
580
515
def move(self, rel_from, rel_to):
581
516
"""Move the item at rel_from to the location at rel_to"""
582
path_from = self._remote_path(rel_from)
583
path_to = self._remote_path(rel_to)
584
self._rename_and_overwrite(path_from, path_to)
517
path_from = self._abspath(rel_from)
518
path_to = self._abspath(rel_to)
520
self._sftp.rename(path_from, path_to)
521
except (IOError, paramiko.SSHException), e:
522
self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
586
524
def delete(self, relpath):
587
525
"""Delete the item at relpath"""
588
path = self._remote_path(relpath)
526
path = self._abspath(relpath)
590
528
self._sftp.remove(path)
591
529
except (IOError, paramiko.SSHException), e:
648
578
# that we have taken the lock.
649
579
return SFTPLock(relpath, self)
651
582
def _unparse_url(self, path=None):
653
584
path = self._path
654
585
path = urllib.quote(path)
655
# handle homedir paths
656
if not path.startswith('/'):
586
if path.startswith('/'):
587
path = '/%2F' + path[1:]
658
590
netloc = urllib.quote(self._host)
659
591
if self._username is not None:
660
592
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
661
593
if self._port is not None:
662
594
netloc = '%s:%d' % (netloc, self._port)
663
596
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
665
598
def _split_url(self, url):
666
(scheme, username, password, host, port, path) = split_url(url)
599
if isinstance(url, unicode):
600
url = url.encode('utf-8')
601
(scheme, netloc, path, params,
602
query, fragment) = urlparse.urlparse(url, allow_fragments=False)
667
603
assert scheme == 'sftp'
604
username = password = host = port = None
606
username, host = netloc.split('@', 1)
608
username, password = username.split(':', 1)
609
password = urllib.unquote(password)
610
username = urllib.unquote(username)
615
host, port = host.rsplit(':', 1)
619
# TODO: Should this be ConnectionError?
620
raise TransportError('%s: invalid port number' % port)
621
host = urllib.unquote(host)
623
path = urllib.unquote(path)
669
625
# the initial slash should be removed from the path, and treated
670
626
# as a homedir relative path (the path begins with a double slash
671
627
# if it is absolute).
672
628
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
673
# RBC 20060118 we are not using this as its too user hostile. instead
674
# we are following lftp and using /~/foo to mean '~/foo'.
675
# handle homedir paths
676
if path.startswith('/~/'):
629
if path.startswith('/'):
680
632
return (username, password, host, port, path)
682
634
def _parse_url(self, url):
840
787
WARNING: This breaks the SFTPClient abstraction, so it
841
788
could easily break against an updated version of paramiko.
843
:param abspath: The remote absolute path where the file should be opened
844
:param mode: The mode permissions bits for the new file
790
:param relpath: The relative path, where the file should be opened
846
path = self._sftp._adjust_cwd(abspath)
792
path = self._sftp._adjust_cwd(self._abspath(relpath))
847
793
attr = SFTPAttributes()
850
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
794
mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
851
795
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
853
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
797
t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
854
798
if t != CMD_HANDLE:
855
799
raise TransportError('Expected an SFTP handle')
856
800
handle = msg.get_string()
857
return SFTPFile(self._sftp, handle, 'wb', -1)
801
return SFTPFile(self._sftp, handle, 'w', -1)
858
802
except (paramiko.SSHException, IOError), e:
859
self._translate_io_exception(e, abspath, ': unable to open',
803
self._translate_io_exception(e, relpath, ': unable to open',
860
804
failure_exc=FileExists)
863
# ------------- server test implementation --------------
867
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
869
STUB_SERVER_KEY = """
870
-----BEGIN RSA PRIVATE KEY-----
871
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
872
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
873
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
874
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
875
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
876
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
877
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
878
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
879
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
880
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
881
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
882
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
883
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
884
-----END RSA PRIVATE KEY-----
888
class SingleListener(threading.Thread):
890
def __init__(self, callback):
891
threading.Thread.__init__(self)
892
self._callback = callback
893
self._socket = socket.socket()
894
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
895
self._socket.bind(('localhost', 0))
896
self._socket.listen(1)
897
self.port = self._socket.getsockname()[1]
898
self.stop_event = threading.Event()
901
s, _ = self._socket.accept()
902
# now close the listen socket
905
self._callback(s, self.stop_event)
907
pass #Ignore socket errors
909
# probably a failed test
910
warning('Exception from within unit test server thread: %r' % x)
913
self.stop_event.set()
914
# use a timeout here, because if the test fails, the server thread may
915
# never notice the stop_event.
919
class SFTPServer(Server):
920
"""Common code for SFTP server facilities."""
923
self._original_vendor = None
925
self._server_homedir = None
926
self._listener = None
928
self._vendor = 'none'
932
def _get_sftp_url(self, path):
933
"""Calculate an sftp url to this server for path."""
934
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
936
def log(self, message):
937
"""StubServer uses this to log when a new server is created."""
938
self.logs.append(message)
940
def _run_server(self, s, stop_event):
941
ssh_server = paramiko.Transport(s)
942
key_file = os.path.join(self._homedir, 'test_rsa.key')
943
file(key_file, 'w').write(STUB_SERVER_KEY)
944
host_key = paramiko.RSAKey.from_private_key_file(key_file)
945
ssh_server.add_server_key(host_key)
946
server = StubServer(self)
947
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
948
StubSFTPServer, root=self._root,
949
home=self._server_homedir)
950
event = threading.Event()
951
ssh_server.start_server(event, server)
953
stop_event.wait(30.0)
957
self._original_vendor = _ssh_vendor
958
_ssh_vendor = self._vendor
959
self._homedir = os.getcwd()
960
if self._server_homedir is None:
961
self._server_homedir = self._homedir
963
# FIXME WINDOWS: _root should be _server_homedir[0]:/
964
self._listener = SingleListener(self._run_server)
965
self._listener.setDaemon(True)
966
self._listener.start()
969
"""See bzrlib.transport.Server.tearDown."""
971
self._listener.stop()
972
_ssh_vendor = self._original_vendor
975
class SFTPFullAbsoluteServer(SFTPServer):
976
"""A test server for sftp transports, using absolute urls and ssh."""
979
"""See bzrlib.transport.Server.get_url."""
980
return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
983
class SFTPServerWithoutSSH(SFTPServer):
984
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
987
super(SFTPServerWithoutSSH, self).__init__()
988
self._vendor = 'loopback'
990
def _run_server(self, sock, stop_event):
991
class FakeChannel(object):
992
def get_transport(self):
994
def get_log_channel(self):
998
def get_hexdump(self):
1003
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1004
root=self._root, home=self._server_homedir)
1005
server.start_subsystem('sftp', None, sock)
1006
server.finish_subsystem()
1009
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1010
"""A test server for sftp transports, using absolute urls."""
1013
"""See bzrlib.transport.Server.get_url."""
1014
return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
1017
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1018
"""A test server for sftp transports, using homedir relative urls."""
1021
"""See bzrlib.transport.Server.get_url."""
1022
return self._get_sftp_url("~/")
1025
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1026
"""A test servere for sftp transports, using absolute urls to non-home."""
1029
self._server_homedir = '/dev/noone/runs/tests/here'
1030
super(SFTPSiblingAbsoluteServer, self).setUp()
1033
def get_test_permutations():
1034
"""Return the permutations to be used in testing."""
1035
return [(SFTPTransport, SFTPAbsoluteServer),
1036
(SFTPTransport, SFTPHomeDirServer),
1037
(SFTPTransport, SFTPSiblingAbsoluteServer),