40
from bzrlib.errors import (FileExists,
43
from bzrlib.errors import (FileExists,
41
44
NoSuchFile, PathNotChild,
45
48
ParamikoNotPresent,
48
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
51
from bzrlib.symbol_versioning import (
49
54
from bzrlib.trace import mutter, warning
50
55
from bzrlib.transport import (
51
register_urlparse_netloc_protocol,
57
import bzrlib.urlutils as urlutils
64
# Disable one particular warning that comes from paramiko in Python2.5; if
65
# this is emitted at the wrong time it tends to cause spurious test failures
66
# or at least noise in the test case::
68
# [1770/7639 in 86s, 1 known failures, 50 skipped, 2 missing features]
69
# test_permissions.TestSftpPermissions.test_new_files
70
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
71
# self.packet.write(struct.pack('>I', n))
72
warnings.filterwarnings('ignore',
73
'integer argument expected, got float',
74
category=DeprecationWarning,
75
module='paramiko.message')
68
86
from paramiko.sftp_file import SFTPFile
71
register_urlparse_netloc_protocol('sftp')
74
# This is a weakref dictionary, so that we can reuse connections
75
# that are still active. Long term, it might be nice to have some
76
# sort of expiration policy, such as disconnect if inactive for
77
# X seconds. But that requires a lot more fanciness.
78
_connected_hosts = weakref.WeakValueDictionary()
81
89
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
82
90
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
83
91
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
86
def clear_connection_cache():
87
"""Remove all hosts from the SFTP connection cache.
89
Primarily useful for test cases wanting to force garbage collection.
91
_connected_hosts.clear()
94
94
class SFTPLock(object):
95
95
"""This fakes a lock in a remote location.
154
152
# up the request itself, rather than us having to worry about it
155
153
_max_request_size = 32768
157
def __init__(self, base, clone_from=None):
158
assert base.startswith('sftp://')
159
self._parse_url(base)
160
base = self._unparse_url()
163
super(SFTPTransport, self).__init__(base)
164
if clone_from is None:
167
# use the same ssh connection, etc
168
self._sftp = clone_from._sftp
169
# super saves 'self.base'
171
def should_cache(self):
173
Return True if the data pulled across should be cached locally.
177
def clone(self, offset=None):
179
Return a new SFTPTransport with root at self.base + offset.
180
We share the same SFTP session between such transports, because it's
181
fairly expensive to set them up.
184
return SFTPTransport(self.base, self)
186
return SFTPTransport(self.abspath(offset), self)
188
def abspath(self, relpath):
190
Return the full url to the given relative path.
192
@param relpath: the relative path or path components
193
@type relpath: str or list
195
return self._unparse_url(self._remote_path(relpath))
155
def __init__(self, base, _from_transport=None):
156
super(SFTPTransport, self).__init__(base,
157
_from_transport=_from_transport)
197
159
def _remote_path(self, relpath):
198
160
"""Return the path to be passed along the sftp protocol for relpath.
200
relpath is a urlencoded string.
202
# FIXME: share the common code across transports
203
assert isinstance(relpath, basestring)
204
relpath = urlutils.unescape(relpath).split('/')
205
basepath = self._path.split('/')
206
if len(basepath) > 0 and basepath[-1] == '':
207
basepath = basepath[:-1]
211
if len(basepath) == 0:
212
# In most filesystems, a request for the parent
213
# of root, just returns root.
221
path = '/'.join(basepath)
222
# mutter('relpath => remotepath %s => %s', relpath, path)
225
def relpath(self, abspath):
226
username, password, host, port, path = self._split_url(abspath)
228
if (username != self._username):
229
error.append('username mismatch')
230
if (host != self._host):
231
error.append('host mismatch')
232
if (port != self._port):
233
error.append('port mismatch')
234
if (not path.startswith(self._path)):
235
error.append('path mismatch')
237
extra = ': ' + ', '.join(error)
238
raise PathNotChild(abspath, self.base, extra=extra)
240
return path[pl:].strip('/')
162
:param relpath: is a urlencoded string.
164
relative = urlutils.unescape(relpath).encode('utf-8')
165
remote_path = self._combine_paths(self._path, relative)
166
# the initial slash should be removed from the path, and treated as a
167
# homedir relative path (the path begins with a double slash if it is
168
# absolute). see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
169
# RBC 20060118 we are not using this as its too user hostile. instead
170
# we are following lftp and using /~/foo to mean '~/foo'
171
# vila--20070602 and leave absolute paths begin with a single slash.
172
if remote_path.startswith('/~/'):
173
remote_path = remote_path[3:]
174
elif remote_path == '/~':
178
def _create_connection(self, credentials=None):
179
"""Create a new connection with the provided credentials.
181
:param credentials: The credentials needed to establish the connection.
183
:return: The created connection and its associated credentials.
185
The credentials are only the password as it may have been entered
186
interactively by the user and may be different from the one provided
187
in base url at transport creation time.
189
if credentials is None:
190
password = self._password
192
password = credentials
194
vendor = ssh._get_ssh_vendor()
195
connection = vendor.connect_sftp(self._user, password,
196
self._host, self._port)
197
return connection, password
200
"""Ensures that a connection is established"""
201
connection = self._get_connection()
202
if connection is None:
203
# First connection ever
204
connection, credentials = self._create_connection()
205
self._set_connection(connection, credentials)
242
208
def has(self, relpath):
244
210
Does the target location exist?
247
self._sftp.stat(self._remote_path(relpath))
213
self._get_sftp().stat(self._remote_path(relpath))
259
225
path = self._remote_path(relpath)
260
f = self._sftp.file(path, mode='rb')
226
f = self._get_sftp().file(path, mode='rb')
261
227
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
264
230
except (IOError, paramiko.SSHException), e:
265
self._translate_io_exception(e, path, ': error retrieving')
231
self._translate_io_exception(e, path, ': error retrieving',
232
failure_exc=errors.ReadError)
267
def readv(self, relpath, offsets):
234
def _readv(self, relpath, offsets):
268
235
"""See Transport.readv()"""
269
236
# We overload the default readv() because we want to use a file
270
237
# that does not have prefetch enabled.
276
243
path = self._remote_path(relpath)
277
fp = self._sftp.file(path, mode='rb')
244
fp = self._get_sftp().file(path, mode='rb')
278
245
readv = getattr(fp, 'readv', None)
280
return self._sftp_readv(fp, offsets)
247
return self._sftp_readv(fp, offsets, relpath)
281
248
mutter('seek and read %s offsets', len(offsets))
282
return self._seek_and_read(fp, offsets)
249
return self._seek_and_read(fp, offsets, relpath)
283
250
except (IOError, paramiko.SSHException), e:
284
251
self._translate_io_exception(e, path, ': error retrieving')
286
def _sftp_readv(self, fp, offsets):
253
def recommended_page_size(self):
254
"""See Transport.recommended_page_size().
256
For SFTP we suggest a large page size to reduce the overhead
257
introduced by latency.
261
def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
287
262
"""Use the readv() member of fp to do async readv.
289
264
And then read them using paramiko.readv(). paramiko.readv()
535
518
local_mode = mode
537
self._sftp.mkdir(abspath, local_mode)
520
self._get_sftp().mkdir(abspath, local_mode)
538
521
if mode is not None:
539
self._sftp.chmod(abspath, mode=mode)
522
# chmod a dir through sftp will erase any sgid bit set
523
# on the server side. So, if the bit mode are already
524
# set, avoid the chmod. If the mode is not fine but
525
# the sgid bit is set, report a warning to the user
526
# with the umask fix.
527
stat = self._get_sftp().lstat(abspath)
528
mode = mode & 0777 # can't set special bits anyway
529
if mode != stat.st_mode & 0777:
530
if stat.st_mode & 06000:
531
warning('About to chmod %s over sftp, which will result'
532
' in its suid or sgid bits being cleared. If'
533
' you want to preserve those bits, change your '
534
' environment on the server to use umask 0%03o.'
535
% (abspath, 0777 - mode))
536
self._get_sftp().chmod(abspath, mode=mode)
540
537
except (paramiko.SSHException, IOError), e:
541
538
self._translate_io_exception(e, abspath, ': unable to mkdir',
542
539
failure_exc=FileExists)
545
542
"""Create a directory at the given path."""
546
543
self._mkdir(self._remote_path(relpath), mode=mode)
548
def _translate_io_exception(self, e, path, more_info='',
545
def open_write_stream(self, relpath, mode=None):
546
"""See Transport.open_write_stream."""
547
# initialise the file to zero-length
548
# this is three round trips, but we don't use this
549
# api more than once per write_group at the moment so
550
# it is a tolerable overhead. Better would be to truncate
551
# the file after opening. RBC 20070805
552
self.put_bytes_non_atomic(relpath, "", mode)
553
abspath = self._remote_path(relpath)
554
# TODO: jam 20060816 paramiko doesn't publicly expose a way to
555
# set the file mode at create time. If it does, use it.
556
# But for now, we just chmod later anyway.
559
handle = self._get_sftp().file(abspath, mode='wb')
560
handle.set_pipelined(True)
561
except (paramiko.SSHException, IOError), e:
562
self._translate_io_exception(e, abspath,
564
_file_streams[self.abspath(relpath)] = handle
565
return FileFileStream(self, relpath, handle)
567
def _translate_io_exception(self, e, path, more_info='',
549
568
failure_exc=PathError):
550
569
"""Translate a paramiko or IOError into a friendlier exception.
687
714
# that we have taken the lock.
688
715
return SFTPLock(relpath, self)
690
def _unparse_url(self, path=None):
693
path = urllib.quote(path)
694
# handle homedir paths
695
if not path.startswith('/'):
697
netloc = urllib.quote(self._host)
698
if self._username is not None:
699
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
700
if self._port is not None:
701
netloc = '%s:%d' % (netloc, self._port)
702
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
704
def _split_url(self, url):
705
(scheme, username, password, host, port, path) = split_url(url)
706
assert scheme == 'sftp'
708
# the initial slash should be removed from the path, and treated
709
# as a homedir relative path (the path begins with a double slash
710
# if it is absolute).
711
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
712
# RBC 20060118 we are not using this as its too user hostile. instead
713
# we are following lftp and using /~/foo to mean '~/foo'.
714
# handle homedir paths
715
if path.startswith('/~/'):
719
return (username, password, host, port, path)
721
def _parse_url(self, url):
722
(self._username, self._password,
723
self._host, self._port, self._path) = self._split_url(url)
725
def _sftp_connect(self):
726
"""Connect to the remote sftp server.
727
After this, self._sftp should have a valid connection (or
728
we raise an TransportError 'could not connect').
730
TODO: Raise a more reasonable ConnectionFailed exception
732
self._sftp = _sftp_connect(self._host, self._port, self._username,
735
717
def _sftp_open_exclusive(self, abspath, mode=None):
736
718
"""Open a remote path exclusively.
758
740
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
759
741
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
761
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
743
t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
762
744
if t != CMD_HANDLE:
763
745
raise TransportError('Expected an SFTP handle')
764
746
handle = msg.get_string()
765
return SFTPFile(self._sftp, handle, 'wb', -1)
747
return SFTPFile(self._get_sftp(), handle, 'wb', -1)
766
748
except (paramiko.SSHException, IOError), e:
767
749
self._translate_io_exception(e, abspath, ': unable to open',
768
750
failure_exc=FileExists)
966
949
ssh_server.start_server(event, server)
970
self._original_vendor = ssh._ssh_vendor
971
ssh._ssh_vendor = self._vendor
952
def setUp(self, backing_server=None):
953
# XXX: TODO: make sftpserver back onto backing_server rather than local
955
if not (backing_server is None or
956
isinstance(backing_server, local.LocalURLServer)):
957
raise AssertionError(
958
"backing_server should not be %r, because this can only serve the "
959
"local current working directory." % (backing_server,))
960
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
961
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
972
962
if sys.platform == 'win32':
973
963
# Win32 needs to use the UNICODE api
974
964
self._homedir = getcwd()
1069
1071
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1070
"""A test servere for sftp transports, using absolute urls to non-home."""
1072
"""A test server for sftp transports where only absolute paths will work.
1074
It does this by serving from a deeply-nested directory that doesn't exist.
1077
def setUp(self, backing_server=None):
1073
1078
self._server_homedir = '/dev/noone/runs/tests/here'
1074
super(SFTPSiblingAbsoluteServer, self).setUp()
1077
def _sftp_connect(host, port, username, password):
1078
"""Connect to the remote sftp server.
1080
:raises: a TransportError 'could not connect'.
1082
:returns: an paramiko.sftp_client.SFTPClient
1084
TODO: Raise a more reasonable ConnectionFailed exception
1086
idx = (host, port, username)
1088
return _connected_hosts[idx]
1092
sftp = _sftp_connect_uncached(host, port, username, password)
1093
_connected_hosts[idx] = sftp
1096
def _sftp_connect_uncached(host, port, username, password):
1097
vendor = ssh._get_ssh_vendor()
1098
sftp = vendor.connect_sftp(username, password, host, port)
1079
super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
1102
1082
def get_test_permutations():