/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/sftp.py

  • Committer: Jelmer Vernooij
  • Date: 2017-06-11 13:48:12 UTC
  • mfrom: (6686 work)
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170611134812-02rfzb60kjyk32cl
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011, 2016, 2017 Canonical Ltd
2
2
#
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
16
16
 
17
17
"""Implementation of Transport over SFTP, using paramiko."""
18
18
 
 
19
from __future__ import absolute_import
 
20
 
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
31
33
import stat
32
34
import sys
33
35
import time
34
 
import urllib
35
 
import urlparse
36
36
import warnings
37
37
 
38
 
from bzrlib import (
 
38
from .. import (
39
39
    config,
40
40
    debug,
41
41
    errors,
42
42
    urlutils,
43
43
    )
44
 
from bzrlib.errors import (FileExists,
45
 
                           NoSuchFile, PathNotChild,
 
44
from ..errors import (FileExists,
 
45
                           NoSuchFile,
46
46
                           TransportError,
47
47
                           LockError,
48
48
                           PathError,
49
49
                           ParamikoNotPresent,
50
50
                           )
51
 
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
52
 
from bzrlib.symbol_versioning import (
53
 
        deprecated_function,
54
 
        )
55
 
from bzrlib.trace import mutter, warning
56
 
from bzrlib.transport import (
 
51
from ..osutils import fancy_rename
 
52
from ..sixish import (
 
53
    zip,
 
54
    )
 
55
from ..trace import mutter, warning
 
56
from ..transport import (
57
57
    FileFileStream,
58
58
    _file_streams,
59
 
    local,
60
 
    Server,
61
59
    ssh,
62
60
    ConnectedTransport,
63
61
    )
77
75
 
78
76
try:
79
77
    import paramiko
80
 
except ImportError, e:
 
78
except ImportError as e:
81
79
    raise ParamikoNotPresent(e)
82
80
else:
83
81
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
87
85
    from paramiko.sftp_file import SFTPFile
88
86
 
89
87
 
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.
 
90
try:
 
91
    from paramiko.py3compat import b as _bad
 
92
    from paramiko.common import asbytes as _bad_asbytes
 
93
except ImportError:
 
94
    pass
 
95
else:
 
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:
 
106
                return tobytes()
 
107
        return s
 
108
 
 
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:
 
118
                return asbytes()
 
119
        return s
 
120
 
 
121
    _bad.func_code = _b_for_broken_paramiko.func_code
 
122
    _bad_asbytes.func_code = _asbytes_for_broken_paramiko.func_code
93
123
 
94
124
 
95
125
class SFTPLock(object):
114
144
        except FileExists:
115
145
            raise LockError('File %r already locked' % (self.path,))
116
146
 
117
 
    def __del__(self):
118
 
        """Should this warn, or actually try to cleanup?"""
119
 
        if self.lock_file:
120
 
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
121
 
            self.unlock()
122
 
 
123
147
    def unlock(self):
124
148
        if not self.lock_file:
125
149
            return
196
220
        """
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.
213
237
        # short readv.
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):
217
241
            if data is None:
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
 
309
        fp.close()
284
310
        if buffered_len:
285
311
            buffered = ''.join(buffered_data)
286
312
            del buffered_data[:]
288
314
        if data_chunks:
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)
321
347
 
322
348
 
323
349
class SFTPTransport(ConnectedTransport):
324
350
    """Transport implementation for SFTP access."""
325
351
 
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
343
368
 
344
 
    def __init__(self, base, _from_transport=None):
345
 
        super(SFTPTransport, self).__init__(base,
346
 
                                            _from_transport=_from_transport)
347
 
 
348
369
    def _remote_path(self, relpath):
349
370
        """Return the path to be passed along the sftp protocol for relpath.
350
371
 
351
372
        :param relpath: is a urlencoded string.
352
373
        """
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.
377
397
        """
378
398
        if credentials is None:
379
 
            password = self._password
 
399
            password = self._parsed_url.password
380
400
        else:
381
401
            password = credentials
382
402
 
383
403
        vendor = ssh._get_ssh_vendor()
384
 
        user = self._user
 
404
        user = self._parsed_url.user
385
405
        if user is None:
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)
391
412
 
 
413
    def disconnect(self):
 
414
        connection = self._get_connection()
 
415
        if connection is not None:
 
416
            connection.close()
 
417
 
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
417
443
        """
418
444
        try:
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.
423
 
            # -- mbp 20090126
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):
427
 
                f.prefetch()
 
447
            size = f.stat().st_size
 
448
            if getattr(f, 'prefetch', None) is not None:
 
449
                f.prefetch(size)
428
450
            return f
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)
432
454
 
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')
462
484
 
463
485
    def recommended_page_size(self):
499
521
            try:
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
520
542
            closed = True
521
543
            self._rename_and_overwrite(tmp_abspath, abspath)
522
544
            return length
523
 
        except Exception, e:
 
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)
557
579
                    writer(fout)
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')
561
583
 
606
628
                                    create_parent_dir=create_parent_dir,
607
629
                                    dir_mode=dir_mode)
608
630
 
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,
611
633
                             dir_mode=None):
 
634
        if not isinstance(raw_bytes, str):
 
635
            raise TypeError(
 
636
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
637
 
612
638
        def writer(fout):
613
 
            fout.write(bytes)
 
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)
630
656
 
631
657
    def _mkdir(self, abspath, mode=None):
632
658
        if mode is None:
633
 
            local_mode = 0777
 
659
            local_mode = 0o777
634
660
        else:
635
661
            local_mode = mode
636
662
        try:
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)
659
685
 
677
703
        try:
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)
735
763
            return result
736
 
        except (IOError, paramiko.SSHException), e:
 
764
        except (IOError, paramiko.SSHException) as e:
737
765
            self._translate_io_exception(e, relpath, ': unable to append')
738
766
 
739
767
    def rename(self, rel_from, rel_to):
741
769
        try:
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))
747
775
 
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))
761
789
 
770
798
        path = self._remote_path(relpath)
771
799
        try:
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')
775
803
 
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
779
807
        return self.base
780
808
 
794
822
        try:
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]
800
828
 
803
831
        path = self._remote_path(relpath)
804
832
        try:
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')
808
836
 
809
837
    def stat(self, relpath):
811
839
        path = self._remote_path(relpath)
812
840
        try:
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')
816
844
 
817
845
    def readlink(self, relpath):
819
847
        path = self._remote_path(relpath)
820
848
        try:
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')
824
852
 
825
853
    def symlink(self, source, link_name):
832
860
                    '%r: unable to create symlink to %r' % (link_name, source),
833
861
                    sftp_retval
834
862
                )
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))
838
866
 
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)
899
927
 
907
935
 
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),