/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 bzrlib/transport/sftp.py

  • Committer: Martin Pool
  • Date: 2007-09-14 06:31:28 UTC
  • mfrom: (2822 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2823.
  • Revision ID: mbp@sourcefrog.net-20070914063128-0p7mh6zfb4pzdg9p
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
34
34
import time
35
35
import urllib
36
36
import urlparse
37
 
import weakref
 
37
import warnings
38
38
 
39
39
from bzrlib import (
40
40
    errors,
48
48
                           ParamikoNotPresent,
49
49
                           )
50
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
 
51
from bzrlib.symbol_versioning import (
 
52
        deprecated_function,
 
53
        zero_ninety,
 
54
        )
51
55
from bzrlib.trace import mutter, warning
52
56
from bzrlib.transport import (
 
57
    FileFileStream,
 
58
    _file_streams,
53
59
    local,
54
60
    register_urlparse_netloc_protocol,
55
61
    Server,
56
 
    split_url,
57
62
    ssh,
58
 
    Transport,
 
63
    ConnectedTransport,
59
64
    )
60
65
 
 
66
# Disable one particular warning that comes from paramiko in Python2.5; if
 
67
# this is emitted at the wrong time it tends to cause spurious test failures
 
68
# or at least noise in the test case::
 
69
#
 
70
# [1770/7639 in 86s, 1 known failures, 50 skipped, 2 missing features]
 
71
# test_permissions.TestSftpPermissions.test_new_files
 
72
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
 
73
#  self.packet.write(struct.pack('>I', n))
 
74
warnings.filterwarnings('ignore',
 
75
        'integer argument expected, got float',
 
76
        category=DeprecationWarning,
 
77
        module='paramiko.message')
 
78
 
61
79
try:
62
80
    import paramiko
63
81
except ImportError, e:
73
91
register_urlparse_netloc_protocol('sftp')
74
92
 
75
93
 
76
 
# This is a weakref dictionary, so that we can reuse connections
77
 
# that are still active. Long term, it might be nice to have some
78
 
# sort of expiration policy, such as disconnect if inactive for
79
 
# X seconds. But that requires a lot more fanciness.
80
 
_connected_hosts = weakref.WeakValueDictionary()
81
 
 
82
 
 
83
94
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
84
95
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
85
96
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
86
97
 
87
98
 
 
99
@deprecated_function(zero_ninety)
88
100
def clear_connection_cache():
89
101
    """Remove all hosts from the SFTP connection cache.
90
102
 
91
103
    Primarily useful for test cases wanting to force garbage collection.
 
104
    We don't have a global connection cache anymore.
92
105
    """
93
 
    _connected_hosts.clear()
94
 
 
95
106
 
96
107
class SFTPLock(object):
97
108
    """This fakes a lock in a remote location.
135
146
            pass
136
147
 
137
148
 
138
 
class SFTPUrlHandling(Transport):
139
 
    """Mix-in that does common handling of SSH/SFTP URLs."""
140
 
 
141
 
    def __init__(self, base):
142
 
        self._parse_url(base)
143
 
        base = self._unparse_url(self._path)
144
 
        if base[-1] != '/':
145
 
            base += '/'
146
 
        super(SFTPUrlHandling, self).__init__(base)
147
 
 
148
 
    def _parse_url(self, url):
149
 
        (self._scheme,
150
 
         self._username, self._password,
151
 
         self._host, self._port, self._path) = self._split_url(url)
152
 
 
153
 
    def _unparse_url(self, path):
154
 
        """Return a URL for a path relative to this transport.
155
 
        """
156
 
        path = urllib.quote(path)
157
 
        # handle homedir paths
158
 
        if not path.startswith('/'):
159
 
            path = "/~/" + path
160
 
        netloc = urllib.quote(self._host)
161
 
        if self._username is not None:
162
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
163
 
        if self._port is not None:
164
 
            netloc = '%s:%d' % (netloc, self._port)
165
 
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
166
 
 
167
 
    def _split_url(self, url):
168
 
        (scheme, username, password, host, port, path) = split_url(url)
169
 
        ## assert scheme == 'sftp'
170
 
 
171
 
        # the initial slash should be removed from the path, and treated
172
 
        # as a homedir relative path (the path begins with a double slash
173
 
        # if it is absolute).
174
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
175
 
        # RBC 20060118 we are not using this as its too user hostile. instead
176
 
        # we are following lftp and using /~/foo to mean '~/foo'.
177
 
        # handle homedir paths
178
 
        if path.startswith('/~/'):
179
 
            path = path[3:]
180
 
        elif path == '/~':
181
 
            path = ''
182
 
        return (scheme, username, password, host, port, path)
183
 
 
184
 
    def abspath(self, relpath):
185
 
        """Return the full url to the given relative path.
186
 
        
187
 
        @param relpath: the relative path or path components
188
 
        @type relpath: str or list
189
 
        """
190
 
        return self._unparse_url(self._remote_path(relpath))
191
 
    
192
 
    def _remote_path(self, relpath):
193
 
        """Return the path to be passed along the sftp protocol for relpath.
194
 
        
195
 
        :param relpath: is a urlencoded string.
196
 
        """
197
 
        return self._combine_paths(self._path, relpath)
198
 
 
199
 
 
200
 
class SFTPTransport(SFTPUrlHandling):
 
149
class SFTPTransport(ConnectedTransport):
201
150
    """Transport implementation for SFTP access."""
202
151
 
203
152
    _do_prefetch = _default_do_prefetch
218
167
    # up the request itself, rather than us having to worry about it
219
168
    _max_request_size = 32768
220
169
 
221
 
    def __init__(self, base, clone_from=None):
222
 
        super(SFTPTransport, self).__init__(base)
223
 
        if clone_from is None:
224
 
            self._sftp_connect()
225
 
        else:
226
 
            # use the same ssh connection, etc
227
 
            self._sftp = clone_from._sftp
228
 
        # super saves 'self.base'
229
 
    
230
 
    def should_cache(self):
231
 
        """
232
 
        Return True if the data pulled across should be cached locally.
233
 
        """
234
 
        return True
235
 
 
236
 
    def clone(self, offset=None):
237
 
        """
238
 
        Return a new SFTPTransport with root at self.base + offset.
239
 
        We share the same SFTP session between such transports, because it's
240
 
        fairly expensive to set them up.
241
 
        """
242
 
        if offset is None:
243
 
            return SFTPTransport(self.base, self)
244
 
        else:
245
 
            return SFTPTransport(self.abspath(offset), self)
 
170
    def __init__(self, base, _from_transport=None):
 
171
        assert base.startswith('sftp://')
 
172
        super(SFTPTransport, self).__init__(base,
 
173
                                            _from_transport=_from_transport)
246
174
 
247
175
    def _remote_path(self, relpath):
248
176
        """Return the path to be passed along the sftp protocol for relpath.
249
177
        
250
 
        relpath is a urlencoded string.
251
 
 
252
 
        :return: a path prefixed with / for regular abspath-based urls, or a
253
 
            path that does not begin with / for urls which begin with /~/.
254
 
        """
255
 
        # how does this work? 
256
 
        # it processes relpath with respect to 
257
 
        # our state:
258
 
        # firstly we create a path to evaluate: 
259
 
        # if relpath is an abspath or homedir path, its the entire thing
260
 
        # otherwise we join our base with relpath
261
 
        # then we eliminate all empty segments (double //'s) outside the first
262
 
        # two elements of the list. This avoids problems with trailing 
263
 
        # slashes, or other abnormalities.
264
 
        # finally we evaluate the entire path in a single pass
265
 
        # '.'s are stripped,
266
 
        # '..' result in popping the left most already 
267
 
        # processed path (which can never be empty because of the check for
268
 
        # abspath and homedir meaning that its not, or that we've used our
269
 
        # path. If the pop would pop the root, we ignore it.
270
 
 
271
 
        # Specific case examinations:
272
 
        # remove the special casefor ~: if the current root is ~/ popping of it
273
 
        # = / thus our seed for a ~ based path is ['', '~']
274
 
        # and if we end up with [''] then we had basically ('', '..') (which is
275
 
        # '/..' so we append '' if the length is one, and assert that the first
276
 
        # element is still ''. Lastly, if we end with ['', '~'] as a prefix for
277
 
        # the output, we've got a homedir path, so we strip that prefix before
278
 
        # '/' joining the resulting list.
279
 
        #
280
 
        # case one: '/' -> ['', ''] cannot shrink
281
 
        # case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
282
 
        #           and pop the second '' for the '..', append 'foo'
283
 
        # case three: '/~/' -> ['', '~', ''] 
284
 
        # case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
285
 
        #           and we want to get '/foo' - the empty path in the middle
286
 
        #           needs to be stripped, then normal path manipulation will 
287
 
        #           work.
288
 
        # case five: '/..' ['', '..'], we want ['', '']
289
 
        #            stripping '' outside the first two is ok
290
 
        #            ignore .. if its too high up
291
 
        #
292
 
        # lastly this code is possibly reusable by FTP, but not reusable by
293
 
        # local paths: ~ is resolvable correctly, nor by HTTP or the smart
294
 
        # server: ~ is resolved remotely.
295
 
        # 
296
 
        # however, a version of this that acts on self.base is possible to be
297
 
        # written which manipulates the URL in canonical form, and would be
298
 
        # reusable for all transports, if a flag for allowing ~/ at all was
299
 
        # provided.
300
 
        assert isinstance(relpath, basestring)
301
 
        relpath = urlutils.unescape(relpath)
302
 
 
303
 
        # case 1)
304
 
        if relpath.startswith('/'):
305
 
            # abspath - normal split is fine.
306
 
            current_path = relpath.split('/')
307
 
        elif relpath.startswith('~/'):
308
 
            # root is homedir based: normal split and prefix '' to remote the
309
 
            # special case
310
 
            current_path = [''].extend(relpath.split('/'))
 
178
        :param relpath: is a urlencoded string.
 
179
        """
 
180
        relative = urlutils.unescape(relpath).encode('utf-8')
 
181
        remote_path = self._combine_paths(self._path, relative)
 
182
        # the initial slash should be removed from the path, and treated as a
 
183
        # homedir relative path (the path begins with a double slash if it is
 
184
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
185
        # RBC 20060118 we are not using this as its too user hostile. instead
 
186
        # we are following lftp and using /~/foo to mean '~/foo'
 
187
        # vila--20070602 and leave absolute paths begin with a single slash.
 
188
        if remote_path.startswith('/~/'):
 
189
            remote_path = remote_path[3:]
 
190
        elif remote_path == '/~':
 
191
            remote_path = ''
 
192
        return remote_path
 
193
 
 
194
    def _create_connection(self, credentials=None):
 
195
        """Create a new connection with the provided credentials.
 
196
 
 
197
        :param credentials: The credentials needed to establish the connection.
 
198
 
 
199
        :return: The created connection and its associated credentials.
 
200
 
 
201
        The credentials are only the password as it may have been entered
 
202
        interactively by the user and may be different from the one provided
 
203
        in base url at transport creation time.
 
204
        """
 
205
        if credentials is None:
 
206
            password = self._password
311
207
        else:
312
 
            # root is from the current directory:
313
 
            if self._path.startswith('/'):
314
 
                # abspath, take the regular split
315
 
                current_path = []
316
 
            else:
317
 
                # homedir based, add the '', '~' not present in self._path
318
 
                current_path = ['', '~']
319
 
            # add our current dir
320
 
            current_path.extend(self._path.split('/'))
321
 
            # add the users relpath
322
 
            current_path.extend(relpath.split('/'))
323
 
        # strip '' segments that are not in the first one - the leading /.
324
 
        to_process = current_path[:1]
325
 
        for segment in current_path[1:]:
326
 
            if segment != '':
327
 
                to_process.append(segment)
328
 
 
329
 
        # process '.' and '..' segments into output_path.
330
 
        output_path = []
331
 
        for segment in to_process:
332
 
            if segment == '..':
333
 
                # directory pop. Remove a directory 
334
 
                # as long as we are not at the root
335
 
                if len(output_path) > 1:
336
 
                    output_path.pop()
337
 
                # else: pass
338
 
                # cannot pop beyond the root, so do nothing
339
 
            elif segment == '.':
340
 
                continue # strip the '.' from the output.
341
 
            else:
342
 
                # this will append '' to output_path for the root elements,
343
 
                # which is appropriate: its why we strip '' in the first pass.
344
 
                output_path.append(segment)
345
 
 
346
 
        # check output special cases:
347
 
        if output_path == ['']:
348
 
            # [''] -> ['', '']
349
 
            output_path = ['', '']
350
 
        elif output_path[:2] == ['', '~']:
351
 
            # ['', '~', ...] -> ...
352
 
            output_path = output_path[2:]
353
 
        path = '/'.join(output_path)
354
 
        return path
355
 
 
356
 
    def relpath(self, abspath):
357
 
        scheme, username, password, host, port, path = self._split_url(abspath)
358
 
        error = []
359
 
        if (username != self._username):
360
 
            error.append('username mismatch')
361
 
        if (host != self._host):
362
 
            error.append('host mismatch')
363
 
        if (port != self._port):
364
 
            error.append('port mismatch')
365
 
        if (not path.startswith(self._path)):
366
 
            error.append('path mismatch')
367
 
        if error:
368
 
            extra = ': ' + ', '.join(error)
369
 
            raise PathNotChild(abspath, self.base, extra=extra)
370
 
        pl = len(self._path)
371
 
        return path[pl:].strip('/')
 
208
            password = credentials
 
209
 
 
210
        vendor = ssh._get_ssh_vendor()
 
211
        connection = vendor.connect_sftp(self._user, password,
 
212
                                         self._host, self._port)
 
213
        return connection, password
 
214
 
 
215
    def _get_sftp(self):
 
216
        """Ensures that a connection is established"""
 
217
        connection = self._get_connection()
 
218
        if connection is None:
 
219
            # First connection ever
 
220
            connection, credentials = self._create_connection()
 
221
            self._set_connection(connection, credentials)
 
222
        return connection
372
223
 
373
224
    def has(self, relpath):
374
225
        """
375
226
        Does the target location exist?
376
227
        """
377
228
        try:
378
 
            self._sftp.stat(self._remote_path(relpath))
 
229
            self._get_sftp().stat(self._remote_path(relpath))
379
230
            return True
380
231
        except IOError:
381
232
            return False
388
239
        """
389
240
        try:
390
241
            path = self._remote_path(relpath)
391
 
            f = self._sftp.file(path, mode='rb')
 
242
            f = self._get_sftp().file(path, mode='rb')
392
243
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
393
244
                f.prefetch()
394
245
            return f
406
257
 
407
258
        try:
408
259
            path = self._remote_path(relpath)
409
 
            fp = self._sftp.file(path, mode='rb')
 
260
            fp = self._get_sftp().file(path, mode='rb')
410
261
            readv = getattr(fp, 'readv', None)
411
262
            if readv:
412
263
                return self._sftp_readv(fp, offsets, relpath)
415
266
        except (IOError, paramiko.SSHException), e:
416
267
            self._translate_io_exception(e, path, ': error retrieving')
417
268
 
 
269
    def recommended_page_size(self):
 
270
        """See Transport.recommended_page_size().
 
271
 
 
272
        For SFTP we suggest a large page size to reduce the overhead
 
273
        introduced by latency.
 
274
        """
 
275
        return 64 * 1024
 
276
 
418
277
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
419
278
        """Use the readv() member of fp to do async readv.
420
279
 
555
414
            # Because we set_pipelined() earlier, theoretically we might 
556
415
            # avoid the round trip for fout.close()
557
416
            if mode is not None:
558
 
                self._sftp.chmod(tmp_abspath, mode)
 
417
                self._get_sftp().chmod(tmp_abspath, mode)
559
418
            fout.close()
560
419
            closed = True
561
420
            self._rename_and_overwrite(tmp_abspath, abspath)
570
429
            try:
571
430
                if not closed:
572
431
                    fout.close()
573
 
                self._sftp.remove(tmp_abspath)
 
432
                self._get_sftp().remove(tmp_abspath)
574
433
            except:
575
434
                # raise the saved except
576
435
                raise e
591
450
            fout = None
592
451
            try:
593
452
                try:
594
 
                    fout = self._sftp.file(abspath, mode='wb')
 
453
                    fout = self._get_sftp().file(abspath, mode='wb')
595
454
                    fout.set_pipelined(True)
596
455
                    writer(fout)
597
456
                except (paramiko.SSHException, IOError), e:
602
461
                # Because we set_pipelined() earlier, theoretically we might 
603
462
                # avoid the round trip for fout.close()
604
463
                if mode is not None:
605
 
                    self._sftp.chmod(abspath, mode)
 
464
                    self._get_sftp().chmod(abspath, mode)
606
465
            finally:
607
466
                if fout is not None:
608
467
                    fout.close()
672
531
        else:
673
532
            local_mode = mode
674
533
        try:
675
 
            self._sftp.mkdir(abspath, local_mode)
 
534
            self._get_sftp().mkdir(abspath, local_mode)
676
535
            if mode is not None:
677
 
                self._sftp.chmod(abspath, mode=mode)
 
536
                self._get_sftp().chmod(abspath, mode=mode)
678
537
        except (paramiko.SSHException, IOError), e:
679
538
            self._translate_io_exception(e, abspath, ': unable to mkdir',
680
539
                failure_exc=FileExists)
683
542
        """Create a directory at the given path."""
684
543
        self._mkdir(self._remote_path(relpath), mode=mode)
685
544
 
 
545
    def open_write_stream(self, relpath, mode=None):
 
546
        """See Transport.open_write_stream."""
 
547
        # initialise the file to zero-length
 
548
        # this is three round trips, but we don't use this 
 
549
        # api more than once per write_group at the moment so 
 
550
        # it is a tolerable overhead. Better would be to truncate
 
551
        # the file after opening. RBC 20070805
 
552
        self.put_bytes_non_atomic(relpath, "", mode)
 
553
        abspath = self._remote_path(relpath)
 
554
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
 
555
        #       set the file mode at create time. If it does, use it.
 
556
        #       But for now, we just chmod later anyway.
 
557
        handle = None
 
558
        try:
 
559
            handle = self._get_sftp().file(abspath, mode='wb')
 
560
            handle.set_pipelined(True)
 
561
        except (paramiko.SSHException, IOError), e:
 
562
            self._translate_io_exception(e, abspath,
 
563
                                         ': unable to open')
 
564
        _file_streams[self.abspath(relpath)] = handle
 
565
        return FileFileStream(self, relpath, handle)
 
566
 
686
567
    def _translate_io_exception(self, e, path, more_info='',
687
568
                                failure_exc=PathError):
688
569
        """Translate a paramiko or IOError into a friendlier exception.
720
601
        """
721
602
        try:
722
603
            path = self._remote_path(relpath)
723
 
            fout = self._sftp.file(path, 'ab')
 
604
            fout = self._get_sftp().file(path, 'ab')
724
605
            if mode is not None:
725
 
                self._sftp.chmod(path, mode)
 
606
                self._get_sftp().chmod(path, mode)
726
607
            result = fout.tell()
727
608
            self._pump(f, fout)
728
609
            return result
732
613
    def rename(self, rel_from, rel_to):
733
614
        """Rename without special overwriting"""
734
615
        try:
735
 
            self._sftp.rename(self._remote_path(rel_from),
 
616
            self._get_sftp().rename(self._remote_path(rel_from),
736
617
                              self._remote_path(rel_to))
737
618
        except (IOError, paramiko.SSHException), e:
738
619
            self._translate_io_exception(e, rel_from,
744
625
        Using the implementation provided by osutils.
745
626
        """
746
627
        try:
 
628
            sftp = self._get_sftp()
747
629
            fancy_rename(abs_from, abs_to,
748
 
                    rename_func=self._sftp.rename,
749
 
                    unlink_func=self._sftp.remove)
 
630
                         rename_func=sftp.rename,
 
631
                         unlink_func=sftp.remove)
750
632
        except (IOError, paramiko.SSHException), e:
751
 
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
 
633
            self._translate_io_exception(e, abs_from,
 
634
                                         ': unable to rename to %r' % (abs_to))
752
635
 
753
636
    def move(self, rel_from, rel_to):
754
637
        """Move the item at rel_from to the location at rel_to"""
760
643
        """Delete the item at relpath"""
761
644
        path = self._remote_path(relpath)
762
645
        try:
763
 
            self._sftp.remove(path)
 
646
            self._get_sftp().remove(path)
764
647
        except (IOError, paramiko.SSHException), e:
765
648
            self._translate_io_exception(e, path, ': unable to delete')
766
649
            
 
650
    def external_url(self):
 
651
        """See bzrlib.transport.Transport.external_url."""
 
652
        # the external path for SFTP is the base
 
653
        return self.base
 
654
 
767
655
    def listable(self):
768
656
        """Return True if this store supports listing."""
769
657
        return True
778
666
        # -- David Allouche 2006-08-11
779
667
        path = self._remote_path(relpath)
780
668
        try:
781
 
            entries = self._sftp.listdir(path)
 
669
            entries = self._get_sftp().listdir(path)
782
670
        except (IOError, paramiko.SSHException), e:
783
671
            self._translate_io_exception(e, path, ': failed to list_dir')
784
672
        return [urlutils.escape(entry) for entry in entries]
787
675
        """See Transport.rmdir."""
788
676
        path = self._remote_path(relpath)
789
677
        try:
790
 
            return self._sftp.rmdir(path)
 
678
            return self._get_sftp().rmdir(path)
791
679
        except (IOError, paramiko.SSHException), e:
792
680
            self._translate_io_exception(e, path, ': failed to rmdir')
793
681
 
795
683
        """Return the stat information for a file."""
796
684
        path = self._remote_path(relpath)
797
685
        try:
798
 
            return self._sftp.stat(path)
 
686
            return self._get_sftp().stat(path)
799
687
        except (IOError, paramiko.SSHException), e:
800
688
            self._translate_io_exception(e, path, ': unable to stat')
801
689
 
825
713
        # that we have taken the lock.
826
714
        return SFTPLock(relpath, self)
827
715
 
828
 
    def _sftp_connect(self):
829
 
        """Connect to the remote sftp server.
830
 
        After this, self._sftp should have a valid connection (or
831
 
        we raise an TransportError 'could not connect').
832
 
 
833
 
        TODO: Raise a more reasonable ConnectionFailed exception
834
 
        """
835
 
        self._sftp = _sftp_connect(self._host, self._port, self._username,
836
 
                self._password)
837
 
 
838
716
    def _sftp_open_exclusive(self, abspath, mode=None):
839
717
        """Open a remote path exclusively.
840
718
 
853
731
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
854
732
        #       However, there is no way to set the permission mode at open 
855
733
        #       time using the sftp_client.file() functionality.
856
 
        path = self._sftp._adjust_cwd(abspath)
 
734
        path = self._get_sftp()._adjust_cwd(abspath)
857
735
        # mutter('sftp abspath %s => %s', abspath, path)
858
736
        attr = SFTPAttributes()
859
737
        if mode is not None:
861
739
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
862
740
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
863
741
        try:
864
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
 
742
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
865
743
            if t != CMD_HANDLE:
866
744
                raise TransportError('Expected an SFTP handle')
867
745
            handle = msg.get_string()
868
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
 
746
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
869
747
        except (paramiko.SSHException, IOError), e:
870
748
            self._translate_io_exception(e, abspath, ': unable to open',
871
749
                failure_exc=FileExists)
1197
1075
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
1198
1076
 
1199
1077
 
1200
 
def _sftp_connect(host, port, username, password):
1201
 
    """Connect to the remote sftp server.
1202
 
 
1203
 
    :raises: a TransportError 'could not connect'.
1204
 
 
1205
 
    :returns: an paramiko.sftp_client.SFTPClient
1206
 
 
1207
 
    TODO: Raise a more reasonable ConnectionFailed exception
1208
 
    """
1209
 
    idx = (host, port, username)
1210
 
    try:
1211
 
        return _connected_hosts[idx]
1212
 
    except KeyError:
1213
 
        pass
1214
 
    
1215
 
    sftp = _sftp_connect_uncached(host, port, username, password)
1216
 
    _connected_hosts[idx] = sftp
1217
 
    return sftp
1218
 
 
1219
 
def _sftp_connect_uncached(host, port, username, password):
1220
 
    vendor = ssh._get_ssh_vendor()
1221
 
    sftp = vendor.connect_sftp(username, password, host, port)
1222
 
    return sftp
1223
 
 
1224
 
 
1225
1078
def get_test_permutations():
1226
1079
    """Return the permutations to be used in testing."""
1227
1080
    return [(SFTPTransport, SFTPAbsoluteServer),