1
# Copyright (C) 2005-2010 Canonical Ltd
1
# Copyright (C) 2005-2011, 2016, 2017 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
17
17
"""Implementation of Transport over SFTP, using paramiko."""
19
from __future__ import absolute_import
19
21
# TODO: Remove the transport-based lock_read and lock_write methods. They'll
20
22
# then raise TransportNotPossible, which will break remote access to any
21
23
# formats which rely on OS-level locks. That should be fine as those formats
44
from bzrlib.errors import (FileExists,
45
NoSuchFile, PathNotChild,
44
from ..errors import (FileExists,
49
49
ParamikoNotPresent,
51
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
52
from bzrlib.symbol_versioning import (
55
from bzrlib.trace import mutter, warning
56
from bzrlib.transport import (
51
from ..osutils import fancy_rename
52
from ..sixish import (
55
from ..trace import mutter, warning
56
from ..transport import (
62
60
ConnectedTransport,
87
85
from paramiko.sftp_file import SFTPFile
90
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
91
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
92
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
88
# GZ 2017-05-25: Some dark hackery to monkeypatch out issues with paramiko's
89
# Python 3 compatibility code. Replace broken b() and asbytes() code.
91
from paramiko.py3compat import b as _bad
92
from paramiko.common import asbytes as _bad_asbytes
96
def _b_for_broken_paramiko(s, encoding='utf8'):
97
"""Hacked b() that does not raise TypeError."""
98
# https://github.com/paramiko/paramiko/issues/967
99
if not isinstance(s, bytes):
100
encode = getattr(s, 'encode', None)
101
if encode is not None:
102
return encode(encoding)
103
# Would like to pass buffer objects along, but have to realise.
104
tobytes = getattr(s, 'tobytes', None)
105
if tobytes is not None:
109
def _asbytes_for_broken_paramiko(s):
110
"""Hacked asbytes() that does not raise Exception."""
111
# https://github.com/paramiko/paramiko/issues/968
112
if not isinstance(s, bytes):
113
encode = getattr(s, 'encode', None)
114
if encode is not None:
115
return encode('utf8')
116
asbytes = getattr(s, 'asbytes', None)
117
if asbytes is not None:
121
_bad.func_code = _b_for_broken_paramiko.func_code
122
_bad_asbytes.func_code = _asbytes_for_broken_paramiko.func_code
95
125
class SFTPLock(object):
114
144
except FileExists:
115
145
raise LockError('File %r already locked' % (self.path,))
118
"""Should this warn, or actually try to cleanup?"""
120
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
123
147
def unlock(self):
124
148
if not self.lock_file:
197
221
requests = self._get_requests()
198
222
offset_iter = iter(self.original_offsets)
199
cur_offset, cur_size = offset_iter.next()
223
cur_offset, cur_size = next(offset_iter)
200
224
# paramiko .readv() yields strings that are in the order of the requests
201
225
# So we track the current request to know where the next data is
202
226
# being returned from.
214
238
data_stream = itertools.chain(fp.readv(requests),
215
239
itertools.repeat(None))
216
for (start, length), data in itertools.izip(requests, data_stream):
240
for (start, length), data in zip(requests, data_stream):
218
242
if cur_coalesced is not None:
219
243
raise errors.ShortReadvError(self.relpath,
270
294
input_start += cur_size
271
295
# Yield the requested data
272
296
yield cur_offset, cur_data
273
cur_offset, cur_size = offset_iter.next()
297
cur_offset, cur_size = next(offset_iter)
274
298
# at this point, we've consumed as much of buffered as we can,
275
299
# so break off the portion that we consumed
276
300
if buffered_offset == len(buffered_data):
281
305
buffered = buffered[buffered_offset:]
282
306
buffered_data = [buffered]
283
307
buffered_len = len(buffered)
308
# now that the data stream is done, close the handle
285
311
buffered = ''.join(buffered_data)
286
312
del buffered_data[:]
289
315
if 'sftp' in debug.debug_flags:
290
316
mutter('SFTP readv left with %d out-of-order bytes',
291
sum(map(lambda x: len(x[1]), data_chunks)))
317
sum(len(x[1]) for x in data_chunks))
292
318
# We've processed all the readv data, at this point, anything we
293
319
# couldn't process is in data_chunks. This doesn't happen often, so
294
320
# this code path isn't optimized
317
343
' We expected %d bytes, but only found %d'
318
344
% (cur_size, len(data)))
319
345
yield cur_offset, data
320
cur_offset, cur_size = offset_iter.next()
346
cur_offset, cur_size = next(offset_iter)
323
349
class SFTPTransport(ConnectedTransport):
324
350
"""Transport implementation for SFTP access."""
326
_do_prefetch = _default_do_prefetch
327
352
# TODO: jam 20060717 Conceivably these could be configurable, either
328
353
# by auto-tuning at run-time, or by a configuration (per host??)
329
354
# but the performance curve is pretty flat, so just going with
341
366
# up the request itself, rather than us having to worry about it
342
367
_max_request_size = 32768
344
def __init__(self, base, _from_transport=None):
345
super(SFTPTransport, self).__init__(base,
346
_from_transport=_from_transport)
348
369
def _remote_path(self, relpath):
349
370
"""Return the path to be passed along the sftp protocol for relpath.
351
372
:param relpath: is a urlencoded string.
353
relative = urlutils.unescape(relpath).encode('utf-8')
354
remote_path = self._combine_paths(self._path, relative)
374
remote_path = self._parsed_url.clone(relpath).path
355
375
# the initial slash should be removed from the path, and treated as a
356
376
# homedir relative path (the path begins with a double slash if it is
357
377
# absolute). see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
376
396
in base url at transport creation time.
378
398
if credentials is None:
379
password = self._password
399
password = self._parsed_url.password
381
401
password = credentials
383
403
vendor = ssh._get_ssh_vendor()
404
user = self._parsed_url.user
386
406
auth = config.AuthenticationConfig()
387
user = auth.get_user('ssh', self._host, self._port)
388
connection = vendor.connect_sftp(self._user, password,
389
self._host, self._port)
407
user = auth.get_user('ssh', self._parsed_url.host,
408
self._parsed_url.port)
409
connection = vendor.connect_sftp(self._parsed_url.user, password,
410
self._parsed_url.host, self._parsed_url.port)
390
411
return connection, (user, password)
413
def disconnect(self):
414
connection = self._get_connection()
415
if connection is not None:
392
418
def _get_sftp(self):
393
419
"""Ensures that a connection is established"""
394
420
connection = self._get_connection()
416
442
:param relpath: The relative path to the file
419
# FIXME: by returning the file directly, we don't pass this
420
# through to report_activity. We could try wrapping the object
421
# before it's returned. For readv and get_bytes it's handled in
422
# the higher-level function.
424
445
path = self._remote_path(relpath)
425
446
f = self._get_sftp().file(path, mode='rb')
426
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
447
size = f.stat().st_size
448
if getattr(f, 'prefetch', None) is not None:
429
except (IOError, paramiko.SSHException), e:
451
except (IOError, paramiko.SSHException) as e:
430
452
self._translate_io_exception(e, path, ': error retrieving',
431
453
failure_exc=errors.ReadError)
457
479
if 'sftp' in debug.debug_flags:
458
480
mutter('seek and read %s offsets', len(offsets))
459
481
return self._seek_and_read(fp, offsets, relpath)
460
except (IOError, paramiko.SSHException), e:
482
except (IOError, paramiko.SSHException) as e:
461
483
self._translate_io_exception(e, path, ': error retrieving')
463
485
def recommended_page_size(self):
500
522
fout.set_pipelined(True)
501
523
length = self._pump(f, fout)
502
except (IOError, paramiko.SSHException), e:
524
except (IOError, paramiko.SSHException) as e:
503
525
self._translate_io_exception(e, tmp_abspath)
504
526
# XXX: This doesn't truly help like we would like it to.
505
527
# The problem is that openssh strips sticky bits. So while we
521
543
self._rename_and_overwrite(tmp_abspath, abspath)
545
except Exception as e:
524
546
# If we fail, try to clean up the temporary file
525
547
# before we throw the exception
526
548
# but don't let another exception mess things up
555
577
fout = self._get_sftp().file(abspath, mode='wb')
556
578
fout.set_pipelined(True)
558
except (paramiko.SSHException, IOError), e:
580
except (paramiko.SSHException, IOError) as e:
559
581
self._translate_io_exception(e, abspath,
560
582
': unable to open')
606
628
create_parent_dir=create_parent_dir,
607
629
dir_mode=dir_mode)
609
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
631
def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
610
632
create_parent_dir=False,
634
if not isinstance(raw_bytes, str):
636
'raw_bytes must be a plain string, not %s' % type(raw_bytes))
612
638
def writer(fout):
639
fout.write(raw_bytes)
614
640
self._put_non_atomic_helper(relpath, writer, mode=mode,
615
641
create_parent_dir=create_parent_dir,
616
642
dir_mode=dir_mode)
644
670
# the sgid bit is set, report a warning to the user
645
671
# with the umask fix.
646
672
stat = self._get_sftp().lstat(abspath)
647
mode = mode & 0777 # can't set special bits anyway
648
if mode != stat.st_mode & 0777:
649
if stat.st_mode & 06000:
673
mode = mode & 0o777 # can't set special bits anyway
674
if mode != stat.st_mode & 0o777:
675
if stat.st_mode & 0o6000:
650
676
warning('About to chmod %s over sftp, which will result'
651
677
' in its suid or sgid bits being cleared. If'
652
678
' you want to preserve those bits, change your '
653
679
' environment on the server to use umask 0%03o.'
654
% (abspath, 0777 - mode))
680
% (abspath, 0o777 - mode))
655
681
self._get_sftp().chmod(abspath, mode=mode)
656
except (paramiko.SSHException, IOError), e:
682
except (paramiko.SSHException, IOError) as e:
657
683
self._translate_io_exception(e, abspath, ': unable to mkdir',
658
684
failure_exc=FileExists)
678
704
handle = self._get_sftp().file(abspath, mode='wb')
679
705
handle.set_pipelined(True)
680
except (paramiko.SSHException, IOError), e:
706
except (paramiko.SSHException, IOError) as e:
681
707
self._translate_io_exception(e, abspath,
682
708
': unable to open')
683
709
_file_streams[self.abspath(relpath)] = handle
715
741
if (e.args[0].startswith('Directory not empty: ')
716
742
or getattr(e, 'errno', None) == errno.ENOTEMPTY):
717
743
raise errors.DirectoryNotEmpty(path, str(e))
744
if e.args == ('Operation unsupported',):
745
raise errors.TransportNotPossible()
718
746
mutter('Raising exception with args %s', e.args)
719
747
if getattr(e, 'errno', None) is not None:
720
748
mutter('Raising exception with errno %s', e.errno)
733
761
result = fout.tell()
734
762
self._pump(f, fout)
736
except (IOError, paramiko.SSHException), e:
764
except (IOError, paramiko.SSHException) as e:
737
765
self._translate_io_exception(e, relpath, ': unable to append')
739
767
def rename(self, rel_from, rel_to):
742
770
self._get_sftp().rename(self._remote_path(rel_from),
743
771
self._remote_path(rel_to))
744
except (IOError, paramiko.SSHException), e:
772
except (IOError, paramiko.SSHException) as e:
745
773
self._translate_io_exception(e, rel_from,
746
774
': unable to rename to %r' % (rel_to))
755
783
fancy_rename(abs_from, abs_to,
756
784
rename_func=sftp.rename,
757
785
unlink_func=sftp.remove)
758
except (IOError, paramiko.SSHException), e:
786
except (IOError, paramiko.SSHException) as e:
759
787
self._translate_io_exception(e, abs_from,
760
788
': unable to rename to %r' % (abs_to))
770
798
path = self._remote_path(relpath)
772
800
self._get_sftp().remove(path)
773
except (IOError, paramiko.SSHException), e:
801
except (IOError, paramiko.SSHException) as e:
774
802
self._translate_io_exception(e, path, ': unable to delete')
776
804
def external_url(self):
777
"""See bzrlib.transport.Transport.external_url."""
805
"""See breezy.transport.Transport.external_url."""
778
806
# the external path for SFTP is the base
795
823
entries = self._get_sftp().listdir(path)
796
824
self._report_activity(sum(map(len, entries)), 'read')
797
except (IOError, paramiko.SSHException), e:
825
except (IOError, paramiko.SSHException) as e:
798
826
self._translate_io_exception(e, path, ': failed to list_dir')
799
827
return [urlutils.escape(entry) for entry in entries]
803
831
path = self._remote_path(relpath)
805
833
return self._get_sftp().rmdir(path)
806
except (IOError, paramiko.SSHException), e:
834
except (IOError, paramiko.SSHException) as e:
807
835
self._translate_io_exception(e, path, ': failed to rmdir')
809
837
def stat(self, relpath):
811
839
path = self._remote_path(relpath)
813
841
return self._get_sftp().lstat(path)
814
except (IOError, paramiko.SSHException), e:
842
except (IOError, paramiko.SSHException) as e:
815
843
self._translate_io_exception(e, path, ': unable to stat')
817
845
def readlink(self, relpath):
819
847
path = self._remote_path(relpath)
821
849
return self._get_sftp().readlink(path)
822
except (IOError, paramiko.SSHException), e:
850
except (IOError, paramiko.SSHException) as e:
823
851
self._translate_io_exception(e, path, ': unable to readlink')
825
853
def symlink(self, source, link_name):
832
860
'%r: unable to create symlink to %r' % (link_name, source),
835
except (IOError, paramiko.SSHException), e:
863
except (IOError, paramiko.SSHException) as e:
836
864
self._translate_io_exception(e, link_name,
837
865
': unable to create symlink to %r' % (source))
893
921
raise TransportError('Expected an SFTP handle')
894
922
handle = msg.get_string()
895
923
return SFTPFile(self._get_sftp(), handle, 'wb', -1)
896
except (paramiko.SSHException, IOError), e:
924
except (paramiko.SSHException, IOError) as e:
897
925
self._translate_io_exception(e, abspath, ': unable to open',
898
926
failure_exc=FileExists)
908
936
def get_test_permutations():
909
937
"""Return the permutations to be used in testing."""
910
from bzrlib.tests import stub_sftp
938
from ..tests import stub_sftp
911
939
return [(SFTPTransport, stub_sftp.SFTPAbsoluteServer),
912
940
(SFTPTransport, stub_sftp.SFTPHomeDirServer),
913
941
(SFTPTransport, stub_sftp.SFTPSiblingAbsoluteServer),