/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: 2018-07-08 14:45:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7036.
  • Revision ID: jelmer@jelmer.uk-20180708144527-codhlvdcdg9y0nji
Fix a bunch of merge tests.

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.__code__ = _b_for_broken_paramiko.__code__
 
122
    _bad_asbytes.__code__ = _asbytes_for_broken_paramiko.__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,
238
262
                if buffered_len > 0:
239
263
                    # We haven't consumed the buffer so far, so put it into
240
264
                    # data_chunks, and continue.
241
 
                    buffered = ''.join(buffered_data)
 
265
                    buffered = b''.join(buffered_data)
242
266
                    data_chunks.append((input_start, buffered))
243
267
                input_start = start
244
268
                buffered_data = [data]
249
273
                # into a single string. We also have the nice property that
250
274
                # when there is only one string ''.join([x]) == x, so there is
251
275
                # no data copying.
252
 
                buffered = ''.join(buffered_data)
 
276
                buffered = b''.join(buffered_data)
253
277
                # Clean out buffered data so that we keep memory
254
278
                # consumption low
255
279
                del buffered_data[:]
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
 
            buffered = ''.join(buffered_data)
 
311
            buffered = b''.join(buffered_data)
286
312
            del buffered_data[:]
287
313
            data_chunks.append((input_start, buffered))
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
 
433
455
    def get_bytes(self, relpath):
434
456
        # reimplement this here so that we can report how many bytes came back
435
 
        f = self.get(relpath)
436
 
        try:
 
457
        with self.get(relpath) as f:
437
458
            bytes = f.read()
438
459
            self._report_activity(len(bytes), 'read')
439
460
            return bytes
440
 
        finally:
441
 
            f.close()
442
461
 
443
462
    def _readv(self, relpath, offsets):
444
463
        """See Transport.readv()"""
457
476
            if 'sftp' in debug.debug_flags:
458
477
                mutter('seek and read %s offsets', len(offsets))
459
478
            return self._seek_and_read(fp, offsets, relpath)
460
 
        except (IOError, paramiko.SSHException), e:
 
479
        except (IOError, paramiko.SSHException) as e:
461
480
            self._translate_io_exception(e, path, ': error retrieving')
462
481
 
463
482
    def recommended_page_size(self):
492
511
    def _put(self, abspath, f, mode=None):
493
512
        """Helper function so both put() and copy_abspaths can reuse the code"""
494
513
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
495
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
514
                        os.getpid(), random.randint(0, 0x7FFFFFFF))
496
515
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
497
516
        closed = False
498
517
        try:
499
518
            try:
500
519
                fout.set_pipelined(True)
501
520
                length = self._pump(f, fout)
502
 
            except (IOError, paramiko.SSHException), e:
 
521
            except (IOError, paramiko.SSHException) as e:
503
522
                self._translate_io_exception(e, tmp_abspath)
504
523
            # XXX: This doesn't truly help like we would like it to.
505
524
            #      The problem is that openssh strips sticky bits. So while we
520
539
            closed = True
521
540
            self._rename_and_overwrite(tmp_abspath, abspath)
522
541
            return length
523
 
        except Exception, e:
 
542
        except Exception as e:
524
543
            # If we fail, try to clean up the temporary file
525
544
            # before we throw the exception
526
545
            # but don't let another exception mess things up
555
574
                    fout = self._get_sftp().file(abspath, mode='wb')
556
575
                    fout.set_pipelined(True)
557
576
                    writer(fout)
558
 
                except (paramiko.SSHException, IOError), e:
 
577
                except (paramiko.SSHException, IOError) as e:
559
578
                    self._translate_io_exception(e, abspath,
560
579
                                                 ': unable to open')
561
580
 
606
625
                                    create_parent_dir=create_parent_dir,
607
626
                                    dir_mode=dir_mode)
608
627
 
609
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
628
    def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
610
629
                             create_parent_dir=False,
611
630
                             dir_mode=None):
 
631
        if not isinstance(raw_bytes, bytes):
 
632
            raise TypeError(
 
633
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
634
 
612
635
        def writer(fout):
613
 
            fout.write(bytes)
 
636
            fout.write(raw_bytes)
614
637
        self._put_non_atomic_helper(relpath, writer, mode=mode,
615
638
                                    create_parent_dir=create_parent_dir,
616
639
                                    dir_mode=dir_mode)
630
653
 
631
654
    def _mkdir(self, abspath, mode=None):
632
655
        if mode is None:
633
 
            local_mode = 0777
 
656
            local_mode = 0o777
634
657
        else:
635
658
            local_mode = mode
636
659
        try:
644
667
                # the sgid bit is set, report a warning to the user
645
668
                # with the umask fix.
646
669
                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:
 
670
                mode = mode & 0o777 # can't set special bits anyway
 
671
                if mode != stat.st_mode & 0o777:
 
672
                    if stat.st_mode & 0o6000:
650
673
                        warning('About to chmod %s over sftp, which will result'
651
674
                                ' in its suid or sgid bits being cleared.  If'
652
675
                                ' you want to preserve those bits, change your '
653
676
                                ' environment on the server to use umask 0%03o.'
654
 
                                % (abspath, 0777 - mode))
 
677
                                % (abspath, 0o777 - mode))
655
678
                    self._get_sftp().chmod(abspath, mode=mode)
656
 
        except (paramiko.SSHException, IOError), e:
 
679
        except (paramiko.SSHException, IOError) as e:
657
680
            self._translate_io_exception(e, abspath, ': unable to mkdir',
658
681
                failure_exc=FileExists)
659
682
 
668
691
        # api more than once per write_group at the moment so
669
692
        # it is a tolerable overhead. Better would be to truncate
670
693
        # the file after opening. RBC 20070805
671
 
        self.put_bytes_non_atomic(relpath, "", mode)
 
694
        self.put_bytes_non_atomic(relpath, b"", mode)
672
695
        abspath = self._remote_path(relpath)
673
696
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
674
697
        #       set the file mode at create time. If it does, use it.
677
700
        try:
678
701
            handle = self._get_sftp().file(abspath, mode='wb')
679
702
            handle.set_pipelined(True)
680
 
        except (paramiko.SSHException, IOError), e:
 
703
        except (paramiko.SSHException, IOError) as e:
681
704
            self._translate_io_exception(e, abspath,
682
705
                                         ': unable to open')
683
706
        _file_streams[self.abspath(relpath)] = handle
715
738
            if (e.args[0].startswith('Directory not empty: ')
716
739
                or getattr(e, 'errno', None) == errno.ENOTEMPTY):
717
740
                raise errors.DirectoryNotEmpty(path, str(e))
 
741
            if e.args == ('Operation unsupported',):
 
742
                raise errors.TransportNotPossible()
718
743
            mutter('Raising exception with args %s', e.args)
719
744
        if getattr(e, 'errno', None) is not None:
720
745
            mutter('Raising exception with errno %s', e.errno)
733
758
            result = fout.tell()
734
759
            self._pump(f, fout)
735
760
            return result
736
 
        except (IOError, paramiko.SSHException), e:
 
761
        except (IOError, paramiko.SSHException) as e:
737
762
            self._translate_io_exception(e, relpath, ': unable to append')
738
763
 
739
764
    def rename(self, rel_from, rel_to):
741
766
        try:
742
767
            self._get_sftp().rename(self._remote_path(rel_from),
743
768
                              self._remote_path(rel_to))
744
 
        except (IOError, paramiko.SSHException), e:
 
769
        except (IOError, paramiko.SSHException) as e:
745
770
            self._translate_io_exception(e, rel_from,
746
771
                    ': unable to rename to %r' % (rel_to))
747
772
 
755
780
            fancy_rename(abs_from, abs_to,
756
781
                         rename_func=sftp.rename,
757
782
                         unlink_func=sftp.remove)
758
 
        except (IOError, paramiko.SSHException), e:
 
783
        except (IOError, paramiko.SSHException) as e:
759
784
            self._translate_io_exception(e, abs_from,
760
785
                                         ': unable to rename to %r' % (abs_to))
761
786
 
770
795
        path = self._remote_path(relpath)
771
796
        try:
772
797
            self._get_sftp().remove(path)
773
 
        except (IOError, paramiko.SSHException), e:
 
798
        except (IOError, paramiko.SSHException) as e:
774
799
            self._translate_io_exception(e, path, ': unable to delete')
775
800
 
776
801
    def external_url(self):
777
 
        """See bzrlib.transport.Transport.external_url."""
 
802
        """See breezy.transport.Transport.external_url."""
778
803
        # the external path for SFTP is the base
779
804
        return self.base
780
805
 
794
819
        try:
795
820
            entries = self._get_sftp().listdir(path)
796
821
            self._report_activity(sum(map(len, entries)), 'read')
797
 
        except (IOError, paramiko.SSHException), e:
 
822
        except (IOError, paramiko.SSHException) as e:
798
823
            self._translate_io_exception(e, path, ': failed to list_dir')
799
824
        return [urlutils.escape(entry) for entry in entries]
800
825
 
803
828
        path = self._remote_path(relpath)
804
829
        try:
805
830
            return self._get_sftp().rmdir(path)
806
 
        except (IOError, paramiko.SSHException), e:
 
831
        except (IOError, paramiko.SSHException) as e:
807
832
            self._translate_io_exception(e, path, ': failed to rmdir')
808
833
 
809
834
    def stat(self, relpath):
811
836
        path = self._remote_path(relpath)
812
837
        try:
813
838
            return self._get_sftp().lstat(path)
814
 
        except (IOError, paramiko.SSHException), e:
 
839
        except (IOError, paramiko.SSHException) as e:
815
840
            self._translate_io_exception(e, path, ': unable to stat')
816
841
 
817
842
    def readlink(self, relpath):
819
844
        path = self._remote_path(relpath)
820
845
        try:
821
846
            return self._get_sftp().readlink(path)
822
 
        except (IOError, paramiko.SSHException), e:
 
847
        except (IOError, paramiko.SSHException) as e:
823
848
            self._translate_io_exception(e, path, ': unable to readlink')
824
849
 
825
850
    def symlink(self, source, link_name):
832
857
                    '%r: unable to create symlink to %r' % (link_name, source),
833
858
                    sftp_retval
834
859
                )
835
 
        except (IOError, paramiko.SSHException), e:
 
860
        except (IOError, paramiko.SSHException) as e:
836
861
            self._translate_io_exception(e, link_name,
837
862
                                         ': unable to create symlink to %r' % (source))
838
863
 
847
872
                self.path = path
848
873
            def unlock(self):
849
874
                pass
 
875
            def __exit__(self, exc_type, exc_val, exc_tb):
 
876
                return False
 
877
            def __enter__(self):
 
878
                pass
850
879
        return BogusLock(relpath)
851
880
 
852
881
    def lock_write(self, relpath):
893
922
                raise TransportError('Expected an SFTP handle')
894
923
            handle = msg.get_string()
895
924
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
896
 
        except (paramiko.SSHException, IOError), e:
 
925
        except (paramiko.SSHException, IOError) as e:
897
926
            self._translate_io_exception(e, abspath, ': unable to open',
898
927
                failure_exc=FileExists)
899
928
 
907
936
 
908
937
def get_test_permutations():
909
938
    """Return the permutations to be used in testing."""
910
 
    from bzrlib.tests import stub_sftp
 
939
    from ..tests import stub_sftp
911
940
    return [(SFTPTransport, stub_sftp.SFTPAbsoluteServer),
912
941
            (SFTPTransport, stub_sftp.SFTPHomeDirServer),
913
942
            (SFTPTransport, stub_sftp.SFTPSiblingAbsoluteServer),