/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-07-28 02:47:10 UTC
  • mfrom: (7519.1.1 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200728024710-a2ylds219f1lsl62
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/388173

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
31
31
import stat
32
32
import sys
33
33
import time
34
 
import urllib
35
 
import urlparse
36
34
import warnings
37
35
 
38
 
from bzrlib import (
 
36
from .. import (
39
37
    config,
40
38
    debug,
41
39
    errors,
42
40
    urlutils,
43
41
    )
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 (
 
42
from ..errors import (FileExists,
 
43
                      NoSuchFile,
 
44
                      TransportError,
 
45
                      LockError,
 
46
                      PathError,
 
47
                      ParamikoNotPresent,
 
48
                      )
 
49
from ..osutils import fancy_rename
 
50
from ..trace import mutter, warning
 
51
from ..transport import (
57
52
    FileFileStream,
58
53
    _file_streams,
59
 
    local,
60
 
    Server,
61
54
    ssh,
62
55
    ConnectedTransport,
63
56
    )
71
64
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
72
65
#  self.packet.write(struct.pack('>I', n))
73
66
warnings.filterwarnings('ignore',
74
 
        'integer argument expected, got float',
75
 
        category=DeprecationWarning,
76
 
        module='paramiko.message')
 
67
                        'integer argument expected, got float',
 
68
                        category=DeprecationWarning,
 
69
                        module='paramiko.message')
77
70
 
78
71
try:
79
72
    import paramiko
80
 
except ImportError, e:
 
73
except ImportError as e:
81
74
    raise ParamikoNotPresent(e)
82
75
else:
83
76
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
84
77
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
85
 
                               SFTP_OK, CMD_HANDLE, CMD_OPEN)
 
78
                               CMD_HANDLE, CMD_OPEN)
86
79
    from paramiko.sftp_attr import SFTPAttributes
87
80
    from paramiko.sftp_file import SFTPFile
88
81
 
89
82
 
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))
 
83
# GZ 2017-05-25: Some dark hackery to monkeypatch out issues with paramiko's
 
84
# Python 3 compatibility code. Replace broken b() and asbytes() code.
 
85
try:
 
86
    from paramiko.py3compat import b as _bad
 
87
    from paramiko.common import asbytes as _bad_asbytes
 
88
except ImportError:
 
89
    pass
 
90
else:
 
91
    def _b_for_broken_paramiko(s, encoding='utf8'):
 
92
        """Hacked b() that does not raise TypeError."""
 
93
        # https://github.com/paramiko/paramiko/issues/967
 
94
        if not isinstance(s, bytes):
 
95
            encode = getattr(s, 'encode', None)
 
96
            if encode is not None:
 
97
                return encode(encoding)
 
98
            # Would like to pass buffer objects along, but have to realise.
 
99
            tobytes = getattr(s, 'tobytes', None)
 
100
            if tobytes is not None:
 
101
                return tobytes()
 
102
        return s
 
103
 
 
104
    def _asbytes_for_broken_paramiko(s):
 
105
        """Hacked asbytes() that does not raise Exception."""
 
106
        # https://github.com/paramiko/paramiko/issues/968
 
107
        if not isinstance(s, bytes):
 
108
            encode = getattr(s, 'encode', None)
 
109
            if encode is not None:
 
110
                return encode('utf8')
 
111
            asbytes = getattr(s, 'asbytes', None)
 
112
            if asbytes is not None:
 
113
                return asbytes()
 
114
        return s
 
115
 
 
116
    _bad.__code__ = _b_for_broken_paramiko.__code__
 
117
    _bad_asbytes.__code__ = _asbytes_for_broken_paramiko.__code__
93
118
 
94
119
 
95
120
class SFTPLock(object):
114
139
        except FileExists:
115
140
            raise LockError('File %r already locked' % (self.path,))
116
141
 
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
142
    def unlock(self):
124
143
        if not self.lock_file:
125
144
            return
169
188
        # as possible, so we don't issues requests <32kB
170
189
        sorted_offsets = sorted(self.original_offsets)
171
190
        coalesced = list(ConnectedTransport._coalesce_offsets(sorted_offsets,
172
 
                                                        limit=0, fudge_factor=0))
 
191
                                                              limit=0, fudge_factor=0))
173
192
        requests = []
174
193
        for c_offset in coalesced:
175
194
            start = c_offset.start
183
202
                start += next_size
184
203
        if 'sftp' in debug.debug_flags:
185
204
            mutter('SFTP.readv(%s) %s offsets => %s coalesced => %s requests',
186
 
                self.relpath, len(sorted_offsets), len(coalesced),
187
 
                len(requests))
 
205
                   self.relpath, len(sorted_offsets), len(coalesced),
 
206
                   len(requests))
188
207
        return requests
189
208
 
190
209
    def request_and_yield_offsets(self, fp):
196
215
        """
197
216
        requests = self._get_requests()
198
217
        offset_iter = iter(self.original_offsets)
199
 
        cur_offset, cur_size = offset_iter.next()
 
218
        cur_offset, cur_size = next(offset_iter)
200
219
        # paramiko .readv() yields strings that are in the order of the requests
201
220
        # So we track the current request to know where the next data is
202
221
        # being returned from.
213
232
        # short readv.
214
233
        data_stream = itertools.chain(fp.readv(requests),
215
234
                                      itertools.repeat(None))
216
 
        for (start, length), data in itertools.izip(requests, data_stream):
 
235
        for (start, length), data in zip(requests, data_stream):
217
236
            if data is None:
218
237
                if cur_coalesced is not None:
219
238
                    raise errors.ShortReadvError(self.relpath,
220
 
                        start, length, len(data))
 
239
                                                 start, length, len(data))
221
240
            if len(data) != length:
222
241
                raise errors.ShortReadvError(self.relpath,
223
 
                    start, length, len(data))
 
242
                                             start, length, len(data))
224
243
            self._report_activity(length, 'read')
225
244
            if last_end is None:
226
245
                # This is the first request, just buffer it
238
257
                if buffered_len > 0:
239
258
                    # We haven't consumed the buffer so far, so put it into
240
259
                    # data_chunks, and continue.
241
 
                    buffered = ''.join(buffered_data)
 
260
                    buffered = b''.join(buffered_data)
242
261
                    data_chunks.append((input_start, buffered))
243
262
                input_start = start
244
263
                buffered_data = [data]
249
268
                # into a single string. We also have the nice property that
250
269
                # when there is only one string ''.join([x]) == x, so there is
251
270
                # no data copying.
252
 
                buffered = ''.join(buffered_data)
 
271
                buffered = b''.join(buffered_data)
253
272
                # Clean out buffered data so that we keep memory
254
273
                # consumption low
255
274
                del buffered_data[:]
270
289
                    input_start += cur_size
271
290
                    # Yield the requested data
272
291
                    yield cur_offset, cur_data
273
 
                    cur_offset, cur_size = offset_iter.next()
 
292
                    try:
 
293
                        cur_offset, cur_size = next(offset_iter)
 
294
                    except StopIteration:
 
295
                        return
274
296
                # at this point, we've consumed as much of buffered as we can,
275
297
                # so break off the portion that we consumed
276
298
                if buffered_offset == len(buffered_data):
281
303
                    buffered = buffered[buffered_offset:]
282
304
                    buffered_data = [buffered]
283
305
                    buffered_len = len(buffered)
 
306
        # now that the data stream is done, close the handle
 
307
        fp.close()
284
308
        if buffered_len:
285
 
            buffered = ''.join(buffered_data)
 
309
            buffered = b''.join(buffered_data)
286
310
            del buffered_data[:]
287
311
            data_chunks.append((input_start, buffered))
288
312
        if data_chunks:
289
313
            if 'sftp' in debug.debug_flags:
290
314
                mutter('SFTP readv left with %d out-of-order bytes',
291
 
                    sum(map(lambda x: len(x[1]), data_chunks)))
 
315
                       sum(len(x[1]) for x in data_chunks))
292
316
            # We've processed all the readv data, at this point, anything we
293
317
            # couldn't process is in data_chunks. This doesn't happen often, so
294
318
            # this code path isn't optimized
314
338
                    data = ''
315
339
                if len(data) != cur_size:
316
340
                    raise AssertionError('We must have miscalulated.'
317
 
                        ' We expected %d bytes, but only found %d'
318
 
                        % (cur_size, len(data)))
 
341
                                         ' We expected %d bytes, but only found %d'
 
342
                                         % (cur_size, len(data)))
319
343
                yield cur_offset, data
320
 
                cur_offset, cur_size = offset_iter.next()
 
344
                try:
 
345
                    cur_offset, cur_size = next(offset_iter)
 
346
                except StopIteration:
 
347
                    return
321
348
 
322
349
 
323
350
class SFTPTransport(ConnectedTransport):
324
351
    """Transport implementation for SFTP access."""
325
352
 
326
 
    _do_prefetch = _default_do_prefetch
327
353
    # TODO: jam 20060717 Conceivably these could be configurable, either
328
354
    #       by auto-tuning at run-time, or by a configuration (per host??)
329
355
    #       but the performance curve is pretty flat, so just going with
341
367
    # up the request itself, rather than us having to worry about it
342
368
    _max_request_size = 32768
343
369
 
344
 
    def __init__(self, base, _from_transport=None):
345
 
        super(SFTPTransport, self).__init__(base,
346
 
                                            _from_transport=_from_transport)
347
 
 
348
370
    def _remote_path(self, relpath):
349
371
        """Return the path to be passed along the sftp protocol for relpath.
350
372
 
351
373
        :param relpath: is a urlencoded string.
352
374
        """
353
 
        relative = urlutils.unescape(relpath).encode('utf-8')
354
 
        remote_path = self._combine_paths(self._path, relative)
 
375
        remote_path = self._parsed_url.clone(relpath).path
355
376
        # the initial slash should be removed from the path, and treated as a
356
377
        # homedir relative path (the path begins with a double slash if it is
357
378
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
376
397
        in base url at transport creation time.
377
398
        """
378
399
        if credentials is None:
379
 
            password = self._password
 
400
            password = self._parsed_url.password
380
401
        else:
381
402
            password = credentials
382
403
 
383
404
        vendor = ssh._get_ssh_vendor()
384
 
        user = self._user
 
405
        user = self._parsed_url.user
385
406
        if user is None:
386
407
            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)
 
408
            user = auth.get_user('ssh', self._parsed_url.host,
 
409
                                 self._parsed_url.port)
 
410
        connection = vendor.connect_sftp(self._parsed_url.user, password,
 
411
                                         self._parsed_url.host, self._parsed_url.port)
390
412
        return connection, (user, password)
391
413
 
 
414
    def disconnect(self):
 
415
        connection = self._get_connection()
 
416
        if connection is not None:
 
417
            connection.close()
 
418
 
392
419
    def _get_sftp(self):
393
420
        """Ensures that a connection is established"""
394
421
        connection = self._get_connection()
416
443
        :param relpath: The relative path to the file
417
444
        """
418
445
        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
446
            path = self._remote_path(relpath)
425
447
            f = self._get_sftp().file(path, mode='rb')
426
 
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
427
 
                f.prefetch()
 
448
            size = f.stat().st_size
 
449
            if getattr(f, 'prefetch', None) is not None:
 
450
                f.prefetch(size)
428
451
            return f
429
 
        except (IOError, paramiko.SSHException), e:
 
452
        except (IOError, paramiko.SSHException) as e:
430
453
            self._translate_io_exception(e, path, ': error retrieving',
431
 
                failure_exc=errors.ReadError)
 
454
                                         failure_exc=errors.ReadError)
432
455
 
433
456
    def get_bytes(self, relpath):
434
457
        # reimplement this here so that we can report how many bytes came back
435
 
        f = self.get(relpath)
436
 
        try:
 
458
        with self.get(relpath) as f:
437
459
            bytes = f.read()
438
460
            self._report_activity(len(bytes), 'read')
439
461
            return bytes
440
 
        finally:
441
 
            f.close()
442
462
 
443
463
    def _readv(self, relpath, offsets):
444
464
        """See Transport.readv()"""
457
477
            if 'sftp' in debug.debug_flags:
458
478
                mutter('seek and read %s offsets', len(offsets))
459
479
            return self._seek_and_read(fp, offsets, relpath)
460
 
        except (IOError, paramiko.SSHException), e:
 
480
        except (IOError, paramiko.SSHException) as e:
461
481
            self._translate_io_exception(e, path, ': error retrieving')
462
482
 
463
483
    def recommended_page_size(self):
492
512
    def _put(self, abspath, f, mode=None):
493
513
        """Helper function so both put() and copy_abspaths can reuse the code"""
494
514
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
495
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
 
515
                                             os.getpid(), random.randint(0, 0x7FFFFFFF))
496
516
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
497
517
        closed = False
498
518
        try:
499
519
            try:
500
520
                fout.set_pipelined(True)
501
521
                length = self._pump(f, fout)
502
 
            except (IOError, paramiko.SSHException), e:
 
522
            except (IOError, paramiko.SSHException) as e:
503
523
                self._translate_io_exception(e, tmp_abspath)
504
524
            # XXX: This doesn't truly help like we would like it to.
505
525
            #      The problem is that openssh strips sticky bits. So while we
520
540
            closed = True
521
541
            self._rename_and_overwrite(tmp_abspath, abspath)
522
542
            return length
523
 
        except Exception, e:
 
543
        except Exception as e:
524
544
            # If we fail, try to clean up the temporary file
525
545
            # before we throw the exception
526
546
            # but don't let another exception mess things up
555
575
                    fout = self._get_sftp().file(abspath, mode='wb')
556
576
                    fout.set_pipelined(True)
557
577
                    writer(fout)
558
 
                except (paramiko.SSHException, IOError), e:
 
578
                except (paramiko.SSHException, IOError) as e:
559
579
                    self._translate_io_exception(e, abspath,
560
580
                                                 ': unable to open')
561
581
 
606
626
                                    create_parent_dir=create_parent_dir,
607
627
                                    dir_mode=dir_mode)
608
628
 
609
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
629
    def put_bytes_non_atomic(self, relpath, raw_bytes, mode=None,
610
630
                             create_parent_dir=False,
611
631
                             dir_mode=None):
 
632
        if not isinstance(raw_bytes, bytes):
 
633
            raise TypeError(
 
634
                'raw_bytes must be a plain string, not %s' % type(raw_bytes))
 
635
 
612
636
        def writer(fout):
613
 
            fout.write(bytes)
 
637
            fout.write(raw_bytes)
614
638
        self._put_non_atomic_helper(relpath, writer, mode=mode,
615
639
                                    create_parent_dir=create_parent_dir,
616
640
                                    dir_mode=dir_mode)
624
648
            st = self.stat(relpath)
625
649
            if stat.S_ISDIR(st.st_mode):
626
650
                for i, basename in enumerate(self.list_dir(relpath)):
627
 
                    queue.insert(i, relpath+'/'+basename)
 
651
                    queue.insert(i, relpath + '/' + basename)
628
652
            else:
629
653
                yield relpath
630
654
 
631
655
    def _mkdir(self, abspath, mode=None):
632
656
        if mode is None:
633
 
            local_mode = 0777
 
657
            local_mode = 0o777
634
658
        else:
635
659
            local_mode = mode
636
660
        try:
644
668
                # the sgid bit is set, report a warning to the user
645
669
                # with the umask fix.
646
670
                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:
 
671
                mode = mode & 0o777  # can't set special bits anyway
 
672
                if mode != stat.st_mode & 0o777:
 
673
                    if stat.st_mode & 0o6000:
650
674
                        warning('About to chmod %s over sftp, which will result'
651
675
                                ' in its suid or sgid bits being cleared.  If'
652
676
                                ' you want to preserve those bits, change your '
653
677
                                ' environment on the server to use umask 0%03o.'
654
 
                                % (abspath, 0777 - mode))
 
678
                                % (abspath, 0o777 - mode))
655
679
                    self._get_sftp().chmod(abspath, mode=mode)
656
 
        except (paramiko.SSHException, IOError), e:
 
680
        except (paramiko.SSHException, IOError) as e:
657
681
            self._translate_io_exception(e, abspath, ': unable to mkdir',
658
 
                failure_exc=FileExists)
 
682
                                         failure_exc=FileExists)
659
683
 
660
684
    def mkdir(self, relpath, mode=None):
661
685
        """Create a directory at the given path."""
668
692
        # api more than once per write_group at the moment so
669
693
        # it is a tolerable overhead. Better would be to truncate
670
694
        # the file after opening. RBC 20070805
671
 
        self.put_bytes_non_atomic(relpath, "", mode)
 
695
        self.put_bytes_non_atomic(relpath, b"", mode)
672
696
        abspath = self._remote_path(relpath)
673
697
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
674
698
        #       set the file mode at create time. If it does, use it.
677
701
        try:
678
702
            handle = self._get_sftp().file(abspath, mode='wb')
679
703
            handle.set_pipelined(True)
680
 
        except (paramiko.SSHException, IOError), e:
 
704
        except (paramiko.SSHException, IOError) as e:
681
705
            self._translate_io_exception(e, abspath,
682
706
                                         ': unable to open')
683
707
        _file_streams[self.abspath(relpath)] = handle
701
725
        self._translate_error(e, path, raise_generic=False)
702
726
        if getattr(e, 'args', None) is not None:
703
727
            if (e.args == ('No such file or directory',) or
704
 
                e.args == ('No such file',)):
 
728
                    e.args == ('No such file',)):
705
729
                raise NoSuchFile(path, str(e) + more_info)
706
730
            if (e.args == ('mkdir failed',) or
707
 
                e.args[0].startswith('syserr: File exists')):
 
731
                    e.args[0].startswith('syserr: File exists')):
708
732
                raise FileExists(path, str(e) + more_info)
709
733
            # strange but true, for the paramiko server.
710
734
            if (e.args == ('Failure',)):
713
737
            # '/srv/bazaar.launchpad.net/blah...: '
714
738
            # [Errno 39] Directory not empty',)
715
739
            if (e.args[0].startswith('Directory not empty: ')
716
 
                or getattr(e, 'errno', None) == errno.ENOTEMPTY):
 
740
                    or getattr(e, 'errno', None) == errno.ENOTEMPTY):
717
741
                raise errors.DirectoryNotEmpty(path, str(e))
 
742
            if e.args == ('Operation unsupported',):
 
743
                raise errors.TransportNotPossible()
718
744
            mutter('Raising exception with args %s', e.args)
719
745
        if getattr(e, 'errno', None) is not None:
720
746
            mutter('Raising exception with errno %s', e.errno)
733
759
            result = fout.tell()
734
760
            self._pump(f, fout)
735
761
            return result
736
 
        except (IOError, paramiko.SSHException), e:
 
762
        except (IOError, paramiko.SSHException) as e:
737
763
            self._translate_io_exception(e, relpath, ': unable to append')
738
764
 
739
765
    def rename(self, rel_from, rel_to):
740
766
        """Rename without special overwriting"""
741
767
        try:
742
768
            self._get_sftp().rename(self._remote_path(rel_from),
743
 
                              self._remote_path(rel_to))
744
 
        except (IOError, paramiko.SSHException), e:
 
769
                                    self._remote_path(rel_to))
 
770
        except (IOError, paramiko.SSHException) as e:
745
771
            self._translate_io_exception(e, rel_from,
746
 
                    ': unable to rename to %r' % (rel_to))
 
772
                                         ': unable to rename to %r' % (rel_to))
747
773
 
748
774
    def _rename_and_overwrite(self, abs_from, abs_to):
749
775
        """Do a fancy rename on the remote server.
755
781
            fancy_rename(abs_from, abs_to,
756
782
                         rename_func=sftp.rename,
757
783
                         unlink_func=sftp.remove)
758
 
        except (IOError, paramiko.SSHException), e:
 
784
        except (IOError, paramiko.SSHException) as e:
759
785
            self._translate_io_exception(e, abs_from,
760
786
                                         ': unable to rename to %r' % (abs_to))
761
787
 
770
796
        path = self._remote_path(relpath)
771
797
        try:
772
798
            self._get_sftp().remove(path)
773
 
        except (IOError, paramiko.SSHException), e:
 
799
        except (IOError, paramiko.SSHException) as e:
774
800
            self._translate_io_exception(e, path, ': unable to delete')
775
801
 
776
802
    def external_url(self):
777
 
        """See bzrlib.transport.Transport.external_url."""
 
803
        """See breezy.transport.Transport.external_url."""
778
804
        # the external path for SFTP is the base
779
805
        return self.base
780
806
 
794
820
        try:
795
821
            entries = self._get_sftp().listdir(path)
796
822
            self._report_activity(sum(map(len, entries)), 'read')
797
 
        except (IOError, paramiko.SSHException), e:
 
823
        except (IOError, paramiko.SSHException) as e:
798
824
            self._translate_io_exception(e, path, ': failed to list_dir')
799
825
        return [urlutils.escape(entry) for entry in entries]
800
826
 
803
829
        path = self._remote_path(relpath)
804
830
        try:
805
831
            return self._get_sftp().rmdir(path)
806
 
        except (IOError, paramiko.SSHException), e:
 
832
        except (IOError, paramiko.SSHException) as e:
807
833
            self._translate_io_exception(e, path, ': failed to rmdir')
808
834
 
809
835
    def stat(self, relpath):
811
837
        path = self._remote_path(relpath)
812
838
        try:
813
839
            return self._get_sftp().lstat(path)
814
 
        except (IOError, paramiko.SSHException), e:
 
840
        except (IOError, paramiko.SSHException) as e:
815
841
            self._translate_io_exception(e, path, ': unable to stat')
816
842
 
817
843
    def readlink(self, relpath):
818
844
        """See Transport.readlink."""
819
845
        path = self._remote_path(relpath)
820
846
        try:
821
 
            return self._get_sftp().readlink(path)
822
 
        except (IOError, paramiko.SSHException), e:
 
847
            return self._get_sftp().readlink(self._remote_path(path))
 
848
        except (IOError, paramiko.SSHException) as e:
823
849
            self._translate_io_exception(e, path, ': unable to readlink')
824
850
 
825
851
    def symlink(self, source, link_name):
826
852
        """See Transport.symlink."""
827
853
        try:
828
854
            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:
 
855
            sftp_retval = conn.symlink(source, self._remote_path(link_name))
 
856
        except (IOError, paramiko.SSHException) as e:
836
857
            self._translate_io_exception(e, link_name,
837
858
                                         ': unable to create symlink to %r' % (source))
838
859
 
845
866
        class BogusLock(object):
846
867
            def __init__(self, path):
847
868
                self.path = path
 
869
 
848
870
            def unlock(self):
849
871
                pass
 
872
 
 
873
            def __exit__(self, exc_type, exc_val, exc_tb):
 
874
                return False
 
875
 
 
876
            def __enter__(self):
 
877
                pass
850
878
        return BogusLock(relpath)
851
879
 
852
880
    def lock_write(self, relpath):
886
914
        if mode is not None:
887
915
            attr.st_mode = mode
888
916
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
889
 
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
 
917
                 | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
890
918
        try:
891
919
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
892
920
            if t != CMD_HANDLE:
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
 
                failure_exc=FileExists)
 
926
                                         failure_exc=FileExists)
899
927
 
900
928
    def _can_roundtrip_unix_modebits(self):
901
929
        if sys.platform == 'win32':
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),