/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: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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,
46
 
                           TransportError,
47
 
                           LockError,
48
 
                           PathError,
49
 
                           ParamikoNotPresent,
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 (
 
44
from ..errors import (FileExists,
 
45
                      NoSuchFile,
 
46
                      TransportError,
 
47
                      LockError,
 
48
                      PathError,
 
49
                      ParamikoNotPresent,
 
50
                      )
 
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
    )
71
69
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
72
70
#  self.packet.write(struct.pack('>I', n))
73
71
warnings.filterwarnings('ignore',
74
 
        'integer argument expected, got float',
75
 
        category=DeprecationWarning,
76
 
        module='paramiko.message')
 
72
                        'integer argument expected, got float',
 
73
                        category=DeprecationWarning,
 
74
                        module='paramiko.message')
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,
84
82
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
85
 
                               SFTP_OK, CMD_HANDLE, CMD_OPEN)
 
83
                               CMD_HANDLE, CMD_OPEN)
86
84
    from paramiko.sftp_attr import SFTPAttributes
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
169
193
        # as possible, so we don't issues requests <32kB
170
194
        sorted_offsets = sorted(self.original_offsets)
171
195
        coalesced = list(ConnectedTransport._coalesce_offsets(sorted_offsets,
172
 
                                                        limit=0, fudge_factor=0))
 
196
                                                              limit=0, fudge_factor=0))
173
197
        requests = []
174
198
        for c_offset in coalesced:
175
199
            start = c_offset.start
183
207
                start += next_size
184
208
        if 'sftp' in debug.debug_flags:
185
209
            mutter('SFTP.readv(%s) %s offsets => %s coalesced => %s requests',
186
 
                self.relpath, len(sorted_offsets), len(coalesced),
187
 
                len(requests))
 
210
                   self.relpath, len(sorted_offsets), len(coalesced),
 
211
                   len(requests))
188
212
        return requests
189
213
 
190
214
    def request_and_yield_offsets(self, fp):
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,
220
 
                        start, length, len(data))
 
244
                                                 start, length, len(data))
221
245
            if len(data) != length:
222
246
                raise errors.ShortReadvError(self.relpath,
223
 
                    start, length, len(data))
 
247
                                             start, length, len(data))
224
248
            self._report_activity(length, 'read')
225
249
            if last_end is None:
226
250
                # This is the first request, just buffer it
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
                    try:
 
298
                        cur_offset, cur_size = next(offset_iter)
 
299
                    except StopIteration:
 
300
                        return
274
301
                # at this point, we've consumed as much of buffered as we can,
275
302
                # so break off the portion that we consumed
276
303
                if buffered_offset == len(buffered_data):
281
308
                    buffered = buffered[buffered_offset:]
282
309
                    buffered_data = [buffered]
283
310
                    buffered_len = len(buffered)
 
311
        # now that the data stream is done, close the handle
 
312
        fp.close()
284
313
        if buffered_len:
285
 
            buffered = ''.join(buffered_data)
 
314
            buffered = b''.join(buffered_data)
286
315
            del buffered_data[:]
287
316
            data_chunks.append((input_start, buffered))
288
317
        if data_chunks:
289
318
            if 'sftp' in debug.debug_flags:
290
319
                mutter('SFTP readv left with %d out-of-order bytes',
291
 
                    sum(map(lambda x: len(x[1]), data_chunks)))
 
320
                       sum(len(x[1]) for x in data_chunks))
292
321
            # We've processed all the readv data, at this point, anything we
293
322
            # couldn't process is in data_chunks. This doesn't happen often, so
294
323
            # this code path isn't optimized
314
343
                    data = ''
315
344
                if len(data) != cur_size:
316
345
                    raise AssertionError('We must have miscalulated.'
317
 
                        ' We expected %d bytes, but only found %d'
318
 
                        % (cur_size, len(data)))
 
346
                                         ' We expected %d bytes, but only found %d'
 
347
                                         % (cur_size, len(data)))
319
348
                yield cur_offset, data
320
 
                cur_offset, cur_size = offset_iter.next()
 
349
                try:
 
350
                    cur_offset, cur_size = next(offset_iter)
 
351
                except StopIteration:
 
352
                    return
321
353
 
322
354
 
323
355
class SFTPTransport(ConnectedTransport):
324
356
    """Transport implementation for SFTP access."""
325
357
 
326
 
    _do_prefetch = _default_do_prefetch
327
358
    # TODO: jam 20060717 Conceivably these could be configurable, either
328
359
    #       by auto-tuning at run-time, or by a configuration (per host??)
329
360
    #       but the performance curve is pretty flat, so just going with
341
372
    # up the request itself, rather than us having to worry about it
342
373
    _max_request_size = 32768
343
374
 
344
 
    def __init__(self, base, _from_transport=None):
345
 
        super(SFTPTransport, self).__init__(base,
346
 
                                            _from_transport=_from_transport)
347
 
 
348
375
    def _remote_path(self, relpath):
349
376
        """Return the path to be passed along the sftp protocol for relpath.
350
377
 
351
378
        :param relpath: is a urlencoded string.
352
379
        """
353
 
        relative = urlutils.unescape(relpath).encode('utf-8')
354
 
        remote_path = self._combine_paths(self._path, relative)
 
380
        remote_path = self._parsed_url.clone(relpath).path
355
381
        # the initial slash should be removed from the path, and treated as a
356
382
        # homedir relative path (the path begins with a double slash if it is
357
383
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
376
402
        in base url at transport creation time.
377
403
        """
378
404
        if credentials is None:
379
 
            password = self._password
 
405
            password = self._parsed_url.password
380
406
        else:
381
407
            password = credentials
382
408
 
383
409
        vendor = ssh._get_ssh_vendor()
384
 
        user = self._user
 
410
        user = self._parsed_url.user
385
411
        if user is None:
386
412
            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)
 
413
            user = auth.get_user('ssh', self._parsed_url.host,
 
414
                                 self._parsed_url.port)
 
415
        connection = vendor.connect_sftp(self._parsed_url.user, password,
 
416
                                         self._parsed_url.host, self._parsed_url.port)
390
417
        return connection, (user, password)
391
418
 
 
419
    def disconnect(self):
 
420
        connection = self._get_connection()
 
421
        if connection is not None:
 
422
            connection.close()
 
423
 
392
424
    def _get_sftp(self):
393
425
        """Ensures that a connection is established"""
394
426
        connection = self._get_connection()
416
448
        :param relpath: The relative path to the file
417
449
        """
418
450
        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
451
            path = self._remote_path(relpath)
425
452
            f = self._get_sftp().file(path, mode='rb')
426
 
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
427
 
                f.prefetch()
 
453
            size = f.stat().st_size
 
454
            if getattr(f, 'prefetch', None) is not None:
 
455
                f.prefetch(size)
428
456
            return f
429
 
        except (IOError, paramiko.SSHException), e:
 
457
        except (IOError, paramiko.SSHException) as e:
430
458
            self._translate_io_exception(e, path, ': error retrieving',
431
 
                failure_exc=errors.ReadError)
 
459
                                         failure_exc=errors.ReadError)
432
460
 
433
461
    def get_bytes(self, relpath):
434
462
        # reimplement this here so that we can report how many bytes came back
435
 
        f = self.get(relpath)
436
 
        try:
 
463
        with self.get(relpath) as f:
437
464
            bytes = f.read()
438
465
            self._report_activity(len(bytes), 'read')
439
466
            return bytes
440
 
        finally:
441
 
            f.close()
442
467
 
443
468
    def _readv(self, relpath, offsets):
444
469
        """See Transport.readv()"""
457
482
            if 'sftp' in debug.debug_flags:
458
483
                mutter('seek and read %s offsets', len(offsets))
459
484
            return self._seek_and_read(fp, offsets, relpath)
460
 
        except (IOError, paramiko.SSHException), e:
 
485
        except (IOError, paramiko.SSHException) as e:
461
486
            self._translate_io_exception(e, path, ': error retrieving')
462
487
 
463
488
    def recommended_page_size(self):
492
517
    def _put(self, abspath, f, mode=None):
493
518
        """Helper function so both put() and copy_abspaths can reuse the code"""
494
519
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
495
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
520
                                             os.getpid(), random.randint(0, 0x7FFFFFFF))
496
521
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
497
522
        closed = False
498
523
        try:
499
524
            try:
500
525
                fout.set_pipelined(True)
501
526
                length = self._pump(f, fout)
502
 
            except (IOError, paramiko.SSHException), e:
 
527
            except (IOError, paramiko.SSHException) as e:
503
528
                self._translate_io_exception(e, tmp_abspath)
504
529
            # XXX: This doesn't truly help like we would like it to.
505
530
            #      The problem is that openssh strips sticky bits. So while we
520
545
            closed = True
521
546
            self._rename_and_overwrite(tmp_abspath, abspath)
522
547
            return length
523
 
        except Exception, e:
 
548
        except Exception as e:
524
549
            # If we fail, try to clean up the temporary file
525
550
            # before we throw the exception
526
551
            # but don't let another exception mess things up
555
580
                    fout = self._get_sftp().file(abspath, mode='wb')
556
581
                    fout.set_pipelined(True)
557
582
                    writer(fout)
558
 
                except (paramiko.SSHException, IOError), e:
 
583
                except (paramiko.SSHException, IOError) as e:
559
584
                    self._translate_io_exception(e, abspath,
560
585
                                                 ': unable to open')
561
586
 
606
631
                                    create_parent_dir=create_parent_dir,
607
632
                                    dir_mode=dir_mode)
608
633
 
609
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
634
    def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
610
635
                             create_parent_dir=False,
611
636
                             dir_mode=None):
 
637
        if not isinstance(raw_bytes, bytes):
 
638
            raise TypeError(
 
639
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
640
 
612
641
        def writer(fout):
613
 
            fout.write(bytes)
 
642
            fout.write(raw_bytes)
614
643
        self._put_non_atomic_helper(relpath, writer, mode=mode,
615
644
                                    create_parent_dir=create_parent_dir,
616
645
                                    dir_mode=dir_mode)
624
653
            st = self.stat(relpath)
625
654
            if stat.S_ISDIR(st.st_mode):
626
655
                for i, basename in enumerate(self.list_dir(relpath)):
627
 
                    queue.insert(i, relpath+'/'+basename)
 
656
                    queue.insert(i, relpath + '/' + basename)
628
657
            else:
629
658
                yield relpath
630
659
 
631
660
    def _mkdir(self, abspath, mode=None):
632
661
        if mode is None:
633
 
            local_mode = 0777
 
662
            local_mode = 0o777
634
663
        else:
635
664
            local_mode = mode
636
665
        try:
644
673
                # the sgid bit is set, report a warning to the user
645
674
                # with the umask fix.
646
675
                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:
 
676
                mode = mode & 0o777  # can't set special bits anyway
 
677
                if mode != stat.st_mode & 0o777:
 
678
                    if stat.st_mode & 0o6000:
650
679
                        warning('About to chmod %s over sftp, which will result'
651
680
                                ' in its suid or sgid bits being cleared.  If'
652
681
                                ' you want to preserve those bits, change your '
653
682
                                ' environment on the server to use umask 0%03o.'
654
 
                                % (abspath, 0777 - mode))
 
683
                                % (abspath, 0o777 - mode))
655
684
                    self._get_sftp().chmod(abspath, mode=mode)
656
 
        except (paramiko.SSHException, IOError), e:
 
685
        except (paramiko.SSHException, IOError) as e:
657
686
            self._translate_io_exception(e, abspath, ': unable to mkdir',
658
 
                failure_exc=FileExists)
 
687
                                         failure_exc=FileExists)
659
688
 
660
689
    def mkdir(self, relpath, mode=None):
661
690
        """Create a directory at the given path."""
668
697
        # api more than once per write_group at the moment so
669
698
        # it is a tolerable overhead. Better would be to truncate
670
699
        # the file after opening. RBC 20070805
671
 
        self.put_bytes_non_atomic(relpath, "", mode)
 
700
        self.put_bytes_non_atomic(relpath, b"", mode)
672
701
        abspath = self._remote_path(relpath)
673
702
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
674
703
        #       set the file mode at create time. If it does, use it.
677
706
        try:
678
707
            handle = self._get_sftp().file(abspath, mode='wb')
679
708
            handle.set_pipelined(True)
680
 
        except (paramiko.SSHException, IOError), e:
 
709
        except (paramiko.SSHException, IOError) as e:
681
710
            self._translate_io_exception(e, abspath,
682
711
                                         ': unable to open')
683
712
        _file_streams[self.abspath(relpath)] = handle
701
730
        self._translate_error(e, path, raise_generic=False)
702
731
        if getattr(e, 'args', None) is not None:
703
732
            if (e.args == ('No such file or directory',) or
704
 
                e.args == ('No such file',)):
 
733
                    e.args == ('No such file',)):
705
734
                raise NoSuchFile(path, str(e) + more_info)
706
735
            if (e.args == ('mkdir failed',) or
707
 
                e.args[0].startswith('syserr: File exists')):
 
736
                    e.args[0].startswith('syserr: File exists')):
708
737
                raise FileExists(path, str(e) + more_info)
709
738
            # strange but true, for the paramiko server.
710
739
            if (e.args == ('Failure',)):
713
742
            # '/srv/bazaar.launchpad.net/blah...: '
714
743
            # [Errno 39] Directory not empty',)
715
744
            if (e.args[0].startswith('Directory not empty: ')
716
 
                or getattr(e, 'errno', None) == errno.ENOTEMPTY):
 
745
                    or getattr(e, 'errno', None) == errno.ENOTEMPTY):
717
746
                raise errors.DirectoryNotEmpty(path, str(e))
 
747
            if e.args == ('Operation unsupported',):
 
748
                raise errors.TransportNotPossible()
718
749
            mutter('Raising exception with args %s', e.args)
719
750
        if getattr(e, 'errno', None) is not None:
720
751
            mutter('Raising exception with errno %s', e.errno)
733
764
            result = fout.tell()
734
765
            self._pump(f, fout)
735
766
            return result
736
 
        except (IOError, paramiko.SSHException), e:
 
767
        except (IOError, paramiko.SSHException) as e:
737
768
            self._translate_io_exception(e, relpath, ': unable to append')
738
769
 
739
770
    def rename(self, rel_from, rel_to):
740
771
        """Rename without special overwriting"""
741
772
        try:
742
773
            self._get_sftp().rename(self._remote_path(rel_from),
743
 
                              self._remote_path(rel_to))
744
 
        except (IOError, paramiko.SSHException), e:
 
774
                                    self._remote_path(rel_to))
 
775
        except (IOError, paramiko.SSHException) as e:
745
776
            self._translate_io_exception(e, rel_from,
746
 
                    ': unable to rename to %r' % (rel_to))
 
777
                                         ': unable to rename to %r' % (rel_to))
747
778
 
748
779
    def _rename_and_overwrite(self, abs_from, abs_to):
749
780
        """Do a fancy rename on the remote server.
755
786
            fancy_rename(abs_from, abs_to,
756
787
                         rename_func=sftp.rename,
757
788
                         unlink_func=sftp.remove)
758
 
        except (IOError, paramiko.SSHException), e:
 
789
        except (IOError, paramiko.SSHException) as e:
759
790
            self._translate_io_exception(e, abs_from,
760
791
                                         ': unable to rename to %r' % (abs_to))
761
792
 
770
801
        path = self._remote_path(relpath)
771
802
        try:
772
803
            self._get_sftp().remove(path)
773
 
        except (IOError, paramiko.SSHException), e:
 
804
        except (IOError, paramiko.SSHException) as e:
774
805
            self._translate_io_exception(e, path, ': unable to delete')
775
806
 
776
807
    def external_url(self):
777
 
        """See bzrlib.transport.Transport.external_url."""
 
808
        """See breezy.transport.Transport.external_url."""
778
809
        # the external path for SFTP is the base
779
810
        return self.base
780
811
 
794
825
        try:
795
826
            entries = self._get_sftp().listdir(path)
796
827
            self._report_activity(sum(map(len, entries)), 'read')
797
 
        except (IOError, paramiko.SSHException), e:
 
828
        except (IOError, paramiko.SSHException) as e:
798
829
            self._translate_io_exception(e, path, ': failed to list_dir')
799
830
        return [urlutils.escape(entry) for entry in entries]
800
831
 
803
834
        path = self._remote_path(relpath)
804
835
        try:
805
836
            return self._get_sftp().rmdir(path)
806
 
        except (IOError, paramiko.SSHException), e:
 
837
        except (IOError, paramiko.SSHException) as e:
807
838
            self._translate_io_exception(e, path, ': failed to rmdir')
808
839
 
809
840
    def stat(self, relpath):
811
842
        path = self._remote_path(relpath)
812
843
        try:
813
844
            return self._get_sftp().lstat(path)
814
 
        except (IOError, paramiko.SSHException), e:
 
845
        except (IOError, paramiko.SSHException) as e:
815
846
            self._translate_io_exception(e, path, ': unable to stat')
816
847
 
817
848
    def readlink(self, relpath):
818
849
        """See Transport.readlink."""
819
850
        path = self._remote_path(relpath)
820
851
        try:
821
 
            return self._get_sftp().readlink(path)
822
 
        except (IOError, paramiko.SSHException), e:
 
852
            return self._get_sftp().readlink(self._remote_path(path))
 
853
        except (IOError, paramiko.SSHException) as e:
823
854
            self._translate_io_exception(e, path, ': unable to readlink')
824
855
 
825
856
    def symlink(self, source, link_name):
826
857
        """See Transport.symlink."""
827
858
        try:
828
859
            conn = self._get_sftp()
829
 
            sftp_retval = conn.symlink(source, link_name)
830
 
            if SFTP_OK != sftp_retval:
831
 
                raise TransportError(
832
 
                    '%r: unable to create symlink to %r' % (link_name, source),
833
 
                    sftp_retval
834
 
                )
835
 
        except (IOError, paramiko.SSHException), e:
 
860
            sftp_retval = conn.symlink(source, self._remote_path(link_name))
 
861
        except (IOError, paramiko.SSHException) as e:
836
862
            self._translate_io_exception(e, link_name,
837
863
                                         ': unable to create symlink to %r' % (source))
838
864
 
845
871
        class BogusLock(object):
846
872
            def __init__(self, path):
847
873
                self.path = path
 
874
 
848
875
            def unlock(self):
849
876
                pass
 
877
 
 
878
            def __exit__(self, exc_type, exc_val, exc_tb):
 
879
                return False
 
880
 
 
881
            def __enter__(self):
 
882
                pass
850
883
        return BogusLock(relpath)
851
884
 
852
885
    def lock_write(self, relpath):
886
919
        if mode is not None:
887
920
            attr.st_mode = mode
888
921
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
889
 
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
 
922
                 | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
890
923
        try:
891
924
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
892
925
            if t != CMD_HANDLE:
893
926
                raise TransportError('Expected an SFTP handle')
894
927
            handle = msg.get_string()
895
928
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
896
 
        except (paramiko.SSHException, IOError), e:
 
929
        except (paramiko.SSHException, IOError) as e:
897
930
            self._translate_io_exception(e, abspath, ': unable to open',
898
 
                failure_exc=FileExists)
 
931
                                         failure_exc=FileExists)
899
932
 
900
933
    def _can_roundtrip_unix_modebits(self):
901
934
        if sys.platform == 'win32':
907
940
 
908
941
def get_test_permutations():
909
942
    """Return the permutations to be used in testing."""
910
 
    from bzrlib.tests import stub_sftp
 
943
    from ..tests import stub_sftp
911
944
    return [(SFTPTransport, stub_sftp.SFTPAbsoluteServer),
912
945
            (SFTPTransport, stub_sftp.SFTPHomeDirServer),
913
946
            (SFTPTransport, stub_sftp.SFTPSiblingAbsoluteServer),