~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Alexander Belchenko
  • Date: 2007-01-04 23:36:44 UTC
  • mfrom: (2224 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2225.
  • Revision ID: bialix@ukr.net-20070104233644-7znkxoj9b0y7ev28
merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
34
34
import time
35
35
import urllib
36
36
import urlparse
 
37
import weakref
37
38
 
38
39
from bzrlib import (
39
40
    errors,
47
48
                           ParamikoNotPresent,
48
49
                           )
49
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
50
 
from bzrlib.symbol_versioning import (
51
 
        deprecated_function,
52
 
        zero_nineteen,
53
 
        )
54
51
from bzrlib.trace import mutter, warning
55
52
from bzrlib.transport import (
56
 
    local,
57
53
    register_urlparse_netloc_protocol,
58
54
    Server,
 
55
    split_url,
59
56
    ssh,
60
 
    ConnectedTransport,
 
57
    Transport,
61
58
    )
62
59
 
63
60
try:
75
72
register_urlparse_netloc_protocol('sftp')
76
73
 
77
74
 
 
75
# This is a weakref dictionary, so that we can reuse connections
 
76
# that are still active. Long term, it might be nice to have some
 
77
# sort of expiration policy, such as disconnect if inactive for
 
78
# X seconds. But that requires a lot more fanciness.
 
79
_connected_hosts = weakref.WeakValueDictionary()
 
80
 
 
81
 
78
82
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
79
83
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
80
84
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
81
85
 
82
86
 
83
 
@deprecated_function(zero_nineteen)
84
87
def clear_connection_cache():
85
88
    """Remove all hosts from the SFTP connection cache.
86
89
 
87
90
    Primarily useful for test cases wanting to force garbage collection.
88
 
    We don't have a global connection cache anymore.
89
91
    """
 
92
    _connected_hosts.clear()
 
93
 
90
94
 
91
95
class SFTPLock(object):
92
96
    """This fakes a lock in a remote location.
130
134
            pass
131
135
 
132
136
 
133
 
class SFTPTransport(ConnectedTransport):
 
137
class SFTPUrlHandling(Transport):
 
138
    """Mix-in that does common handling of SSH/SFTP URLs."""
 
139
 
 
140
    def __init__(self, base):
 
141
        self._parse_url(base)
 
142
        base = self._unparse_url(self._path)
 
143
        if base[-1] != '/':
 
144
            base += '/'
 
145
        super(SFTPUrlHandling, self).__init__(base)
 
146
 
 
147
    def _parse_url(self, url):
 
148
        (self._scheme,
 
149
         self._username, self._password,
 
150
         self._host, self._port, self._path) = self._split_url(url)
 
151
 
 
152
    def _unparse_url(self, path):
 
153
        """Return a URL for a path relative to this transport.
 
154
        """
 
155
        path = urllib.quote(path)
 
156
        # handle homedir paths
 
157
        if not path.startswith('/'):
 
158
            path = "/~/" + path
 
159
        netloc = urllib.quote(self._host)
 
160
        if self._username is not None:
 
161
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
162
        if self._port is not None:
 
163
            netloc = '%s:%d' % (netloc, self._port)
 
164
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
 
165
 
 
166
    def _split_url(self, url):
 
167
        (scheme, username, password, host, port, path) = split_url(url)
 
168
        ## assert scheme == 'sftp'
 
169
 
 
170
        # the initial slash should be removed from the path, and treated
 
171
        # as a homedir relative path (the path begins with a double slash
 
172
        # if it is absolute).
 
173
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
174
        # RBC 20060118 we are not using this as its too user hostile. instead
 
175
        # we are following lftp and using /~/foo to mean '~/foo'.
 
176
        # handle homedir paths
 
177
        if path.startswith('/~/'):
 
178
            path = path[3:]
 
179
        elif path == '/~':
 
180
            path = ''
 
181
        return (scheme, username, password, host, port, path)
 
182
 
 
183
    def abspath(self, relpath):
 
184
        """Return the full url to the given relative path.
 
185
        
 
186
        @param relpath: the relative path or path components
 
187
        @type relpath: str or list
 
188
        """
 
189
        return self._unparse_url(self._remote_path(relpath))
 
190
    
 
191
    def _remote_path(self, relpath):
 
192
        """Return the path to be passed along the sftp protocol for relpath.
 
193
        
 
194
        :param relpath: is a urlencoded string.
 
195
        """
 
196
        return self._combine_paths(self._path, relpath)
 
197
 
 
198
 
 
199
class SFTPTransport(SFTPUrlHandling):
134
200
    """Transport implementation for SFTP access."""
135
201
 
136
202
    _do_prefetch = _default_do_prefetch
151
217
    # up the request itself, rather than us having to worry about it
152
218
    _max_request_size = 32768
153
219
 
154
 
    def __init__(self, base, _from_transport=None):
155
 
        assert base.startswith('sftp://')
156
 
        super(SFTPTransport, self).__init__(base,
157
 
                                            _from_transport=_from_transport)
158
 
 
159
 
    def _remote_path(self, relpath):
160
 
        """Return the path to be passed along the sftp protocol for relpath.
161
 
        
162
 
        :param relpath: is a urlencoded string.
163
 
        """
164
 
        relative = urlutils.unescape(relpath).encode('utf-8')
165
 
        remote_path = self._combine_paths(self._path, relative)
166
 
        # the initial slash should be removed from the path, and treated as a
167
 
        # homedir relative path (the path begins with a double slash if it is
168
 
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
169
 
        # RBC 20060118 we are not using this as its too user hostile. instead
170
 
        # we are following lftp and using /~/foo to mean '~/foo'
171
 
        # vila--20070602 and leave absolute paths begin with a single slash.
172
 
        if remote_path.startswith('/~/'):
173
 
            remote_path = remote_path[3:]
174
 
        elif remote_path == '/~':
175
 
            remote_path = ''
176
 
        return remote_path
177
 
 
178
 
    def _create_connection(self, credentials=None):
179
 
        """Create a new connection with the provided credentials.
180
 
 
181
 
        :param credentials: The credentials needed to establish the connection.
182
 
 
183
 
        :return: The created connection and its associated credentials.
184
 
 
185
 
        The credentials are only the password as it may have been entered
186
 
        interactively by the user and may be different from the one provided
187
 
        in base url at transport creation time.
188
 
        """
189
 
        if credentials is None:
190
 
            password = self._password
 
220
    def __init__(self, base, clone_from=None):
 
221
        super(SFTPTransport, self).__init__(base)
 
222
        if clone_from is None:
 
223
            self._sftp_connect()
191
224
        else:
192
 
            password = credentials
193
 
 
194
 
        vendor = ssh._get_ssh_vendor()
195
 
        connection = vendor.connect_sftp(self._user, password,
196
 
                                         self._host, self._port)
197
 
        return connection, password
198
 
 
199
 
    def _get_sftp(self):
200
 
        """Ensures that a connection is established"""
201
 
        connection = self._get_connection()
202
 
        if connection is None:
203
 
            # First connection ever
204
 
            connection, credentials = self._create_connection()
205
 
            self._set_connection(connection, credentials)
206
 
        return connection
207
 
 
208
 
 
 
225
            # use the same ssh connection, etc
 
226
            self._sftp = clone_from._sftp
 
227
        # super saves 'self.base'
 
228
    
209
229
    def should_cache(self):
210
230
        """
211
231
        Return True if the data pulled across should be cached locally.
212
232
        """
213
233
        return True
214
234
 
 
235
    def clone(self, offset=None):
 
236
        """
 
237
        Return a new SFTPTransport with root at self.base + offset.
 
238
        We share the same SFTP session between such transports, because it's
 
239
        fairly expensive to set them up.
 
240
        """
 
241
        if offset is None:
 
242
            return SFTPTransport(self.base, self)
 
243
        else:
 
244
            return SFTPTransport(self.abspath(offset), self)
 
245
 
 
246
    def _remote_path(self, relpath):
 
247
        """Return the path to be passed along the sftp protocol for relpath.
 
248
        
 
249
        relpath is a urlencoded string.
 
250
 
 
251
        :return: a path prefixed with / for regular abspath-based urls, or a
 
252
            path that does not begin with / for urls which begin with /~/.
 
253
        """
 
254
        # how does this work? 
 
255
        # it processes relpath with respect to 
 
256
        # our state:
 
257
        # firstly we create a path to evaluate: 
 
258
        # if relpath is an abspath or homedir path, its the entire thing
 
259
        # otherwise we join our base with relpath
 
260
        # then we eliminate all empty segments (double //'s) outside the first
 
261
        # two elements of the list. This avoids problems with trailing 
 
262
        # slashes, or other abnormalities.
 
263
        # finally we evaluate the entire path in a single pass
 
264
        # '.'s are stripped,
 
265
        # '..' result in popping the left most already 
 
266
        # processed path (which can never be empty because of the check for
 
267
        # abspath and homedir meaning that its not, or that we've used our
 
268
        # path. If the pop would pop the root, we ignore it.
 
269
 
 
270
        # Specific case examinations:
 
271
        # remove the special casefor ~: if the current root is ~/ popping of it
 
272
        # = / thus our seed for a ~ based path is ['', '~']
 
273
        # and if we end up with [''] then we had basically ('', '..') (which is
 
274
        # '/..' so we append '' if the length is one, and assert that the first
 
275
        # element is still ''. Lastly, if we end with ['', '~'] as a prefix for
 
276
        # the output, we've got a homedir path, so we strip that prefix before
 
277
        # '/' joining the resulting list.
 
278
        #
 
279
        # case one: '/' -> ['', ''] cannot shrink
 
280
        # case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
 
281
        #           and pop the second '' for the '..', append 'foo'
 
282
        # case three: '/~/' -> ['', '~', ''] 
 
283
        # case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
 
284
        #           and we want to get '/foo' - the empty path in the middle
 
285
        #           needs to be stripped, then normal path manipulation will 
 
286
        #           work.
 
287
        # case five: '/..' ['', '..'], we want ['', '']
 
288
        #            stripping '' outside the first two is ok
 
289
        #            ignore .. if its too high up
 
290
        #
 
291
        # lastly this code is possibly reusable by FTP, but not reusable by
 
292
        # local paths: ~ is resolvable correctly, nor by HTTP or the smart
 
293
        # server: ~ is resolved remotely.
 
294
        # 
 
295
        # however, a version of this that acts on self.base is possible to be
 
296
        # written which manipulates the URL in canonical form, and would be
 
297
        # reusable for all transports, if a flag for allowing ~/ at all was
 
298
        # provided.
 
299
        assert isinstance(relpath, basestring)
 
300
        relpath = urlutils.unescape(relpath)
 
301
 
 
302
        # case 1)
 
303
        if relpath.startswith('/'):
 
304
            # abspath - normal split is fine.
 
305
            current_path = relpath.split('/')
 
306
        elif relpath.startswith('~/'):
 
307
            # root is homedir based: normal split and prefix '' to remote the
 
308
            # special case
 
309
            current_path = [''].extend(relpath.split('/'))
 
310
        else:
 
311
            # root is from the current directory:
 
312
            if self._path.startswith('/'):
 
313
                # abspath, take the regular split
 
314
                current_path = []
 
315
            else:
 
316
                # homedir based, add the '', '~' not present in self._path
 
317
                current_path = ['', '~']
 
318
            # add our current dir
 
319
            current_path.extend(self._path.split('/'))
 
320
            # add the users relpath
 
321
            current_path.extend(relpath.split('/'))
 
322
        # strip '' segments that are not in the first one - the leading /.
 
323
        to_process = current_path[:1]
 
324
        for segment in current_path[1:]:
 
325
            if segment != '':
 
326
                to_process.append(segment)
 
327
 
 
328
        # process '.' and '..' segments into output_path.
 
329
        output_path = []
 
330
        for segment in to_process:
 
331
            if segment == '..':
 
332
                # directory pop. Remove a directory 
 
333
                # as long as we are not at the root
 
334
                if len(output_path) > 1:
 
335
                    output_path.pop()
 
336
                # else: pass
 
337
                # cannot pop beyond the root, so do nothing
 
338
            elif segment == '.':
 
339
                continue # strip the '.' from the output.
 
340
            else:
 
341
                # this will append '' to output_path for the root elements,
 
342
                # which is appropriate: its why we strip '' in the first pass.
 
343
                output_path.append(segment)
 
344
 
 
345
        # check output special cases:
 
346
        if output_path == ['']:
 
347
            # [''] -> ['', '']
 
348
            output_path = ['', '']
 
349
        elif output_path[:2] == ['', '~']:
 
350
            # ['', '~', ...] -> ...
 
351
            output_path = output_path[2:]
 
352
        path = '/'.join(output_path)
 
353
        return path
 
354
 
 
355
    def relpath(self, abspath):
 
356
        scheme, username, password, host, port, path = self._split_url(abspath)
 
357
        error = []
 
358
        if (username != self._username):
 
359
            error.append('username mismatch')
 
360
        if (host != self._host):
 
361
            error.append('host mismatch')
 
362
        if (port != self._port):
 
363
            error.append('port mismatch')
 
364
        if (not path.startswith(self._path)):
 
365
            error.append('path mismatch')
 
366
        if error:
 
367
            extra = ': ' + ', '.join(error)
 
368
            raise PathNotChild(abspath, self.base, extra=extra)
 
369
        pl = len(self._path)
 
370
        return path[pl:].strip('/')
 
371
 
215
372
    def has(self, relpath):
216
373
        """
217
374
        Does the target location exist?
218
375
        """
219
376
        try:
220
 
            self._get_sftp().stat(self._remote_path(relpath))
 
377
            self._sftp.stat(self._remote_path(relpath))
221
378
            return True
222
379
        except IOError:
223
380
            return False
230
387
        """
231
388
        try:
232
389
            path = self._remote_path(relpath)
233
 
            f = self._get_sftp().file(path, mode='rb')
 
390
            f = self._sftp.file(path, mode='rb')
234
391
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
235
392
                f.prefetch()
236
393
            return f
237
394
        except (IOError, paramiko.SSHException), e:
238
 
            self._translate_io_exception(e, path, ': error retrieving',
239
 
                failure_exc=errors.ReadError)
 
395
            self._translate_io_exception(e, path, ': error retrieving')
240
396
 
241
397
    def readv(self, relpath, offsets):
242
398
        """See Transport.readv()"""
248
404
 
249
405
        try:
250
406
            path = self._remote_path(relpath)
251
 
            fp = self._get_sftp().file(path, mode='rb')
 
407
            fp = self._sftp.file(path, mode='rb')
252
408
            readv = getattr(fp, 'readv', None)
253
409
            if readv:
254
410
                return self._sftp_readv(fp, offsets, relpath)
397
553
            # Because we set_pipelined() earlier, theoretically we might 
398
554
            # avoid the round trip for fout.close()
399
555
            if mode is not None:
400
 
                self._get_sftp().chmod(tmp_abspath, mode)
 
556
                self._sftp.chmod(tmp_abspath, mode)
401
557
            fout.close()
402
558
            closed = True
403
559
            self._rename_and_overwrite(tmp_abspath, abspath)
412
568
            try:
413
569
                if not closed:
414
570
                    fout.close()
415
 
                self._get_sftp().remove(tmp_abspath)
 
571
                self._sftp.remove(tmp_abspath)
416
572
            except:
417
573
                # raise the saved except
418
574
                raise e
433
589
            fout = None
434
590
            try:
435
591
                try:
436
 
                    fout = self._get_sftp().file(abspath, mode='wb')
 
592
                    fout = self._sftp.file(abspath, mode='wb')
437
593
                    fout.set_pipelined(True)
438
594
                    writer(fout)
439
595
                except (paramiko.SSHException, IOError), e:
444
600
                # Because we set_pipelined() earlier, theoretically we might 
445
601
                # avoid the round trip for fout.close()
446
602
                if mode is not None:
447
 
                    self._get_sftp().chmod(abspath, mode)
 
603
                    self._sftp.chmod(abspath, mode)
448
604
            finally:
449
605
                if fout is not None:
450
606
                    fout.close()
514
670
        else:
515
671
            local_mode = mode
516
672
        try:
517
 
            self._get_sftp().mkdir(abspath, local_mode)
 
673
            self._sftp.mkdir(abspath, local_mode)
518
674
            if mode is not None:
519
 
                self._get_sftp().chmod(abspath, mode=mode)
 
675
                self._sftp.chmod(abspath, mode=mode)
520
676
        except (paramiko.SSHException, IOError), e:
521
677
            self._translate_io_exception(e, abspath, ': unable to mkdir',
522
678
                failure_exc=FileExists)
525
681
        """Create a directory at the given path."""
526
682
        self._mkdir(self._remote_path(relpath), mode=mode)
527
683
 
528
 
    def _translate_io_exception(self, e, path, more_info='',
 
684
    def _translate_io_exception(self, e, path, more_info='', 
529
685
                                failure_exc=PathError):
530
686
        """Translate a paramiko or IOError into a friendlier exception.
531
687
 
562
718
        """
563
719
        try:
564
720
            path = self._remote_path(relpath)
565
 
            fout = self._get_sftp().file(path, 'ab')
 
721
            fout = self._sftp.file(path, 'ab')
566
722
            if mode is not None:
567
 
                self._get_sftp().chmod(path, mode)
 
723
                self._sftp.chmod(path, mode)
568
724
            result = fout.tell()
569
725
            self._pump(f, fout)
570
726
            return result
574
730
    def rename(self, rel_from, rel_to):
575
731
        """Rename without special overwriting"""
576
732
        try:
577
 
            self._get_sftp().rename(self._remote_path(rel_from),
 
733
            self._sftp.rename(self._remote_path(rel_from),
578
734
                              self._remote_path(rel_to))
579
735
        except (IOError, paramiko.SSHException), e:
580
736
            self._translate_io_exception(e, rel_from,
586
742
        Using the implementation provided by osutils.
587
743
        """
588
744
        try:
589
 
            sftp = self._get_sftp()
590
745
            fancy_rename(abs_from, abs_to,
591
 
                         rename_func=sftp.rename,
592
 
                         unlink_func=sftp.remove)
 
746
                    rename_func=self._sftp.rename,
 
747
                    unlink_func=self._sftp.remove)
593
748
        except (IOError, paramiko.SSHException), e:
594
 
            self._translate_io_exception(e, abs_from,
595
 
                                         ': unable to rename to %r' % (abs_to))
 
749
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
596
750
 
597
751
    def move(self, rel_from, rel_to):
598
752
        """Move the item at rel_from to the location at rel_to"""
604
758
        """Delete the item at relpath"""
605
759
        path = self._remote_path(relpath)
606
760
        try:
607
 
            self._get_sftp().remove(path)
 
761
            self._sftp.remove(path)
608
762
        except (IOError, paramiko.SSHException), e:
609
763
            self._translate_io_exception(e, path, ': unable to delete')
610
764
            
611
 
    def external_url(self):
612
 
        """See bzrlib.transport.Transport.external_url."""
613
 
        # the external path for SFTP is the base
614
 
        return self.base
615
 
 
616
765
    def listable(self):
617
766
        """Return True if this store supports listing."""
618
767
        return True
627
776
        # -- David Allouche 2006-08-11
628
777
        path = self._remote_path(relpath)
629
778
        try:
630
 
            entries = self._get_sftp().listdir(path)
 
779
            entries = self._sftp.listdir(path)
631
780
        except (IOError, paramiko.SSHException), e:
632
781
            self._translate_io_exception(e, path, ': failed to list_dir')
633
782
        return [urlutils.escape(entry) for entry in entries]
636
785
        """See Transport.rmdir."""
637
786
        path = self._remote_path(relpath)
638
787
        try:
639
 
            return self._get_sftp().rmdir(path)
 
788
            return self._sftp.rmdir(path)
640
789
        except (IOError, paramiko.SSHException), e:
641
790
            self._translate_io_exception(e, path, ': failed to rmdir')
642
791
 
644
793
        """Return the stat information for a file."""
645
794
        path = self._remote_path(relpath)
646
795
        try:
647
 
            return self._get_sftp().stat(path)
 
796
            return self._sftp.stat(path)
648
797
        except (IOError, paramiko.SSHException), e:
649
798
            self._translate_io_exception(e, path, ': unable to stat')
650
799
 
674
823
        # that we have taken the lock.
675
824
        return SFTPLock(relpath, self)
676
825
 
 
826
    def _sftp_connect(self):
 
827
        """Connect to the remote sftp server.
 
828
        After this, self._sftp should have a valid connection (or
 
829
        we raise an TransportError 'could not connect').
 
830
 
 
831
        TODO: Raise a more reasonable ConnectionFailed exception
 
832
        """
 
833
        self._sftp = _sftp_connect(self._host, self._port, self._username,
 
834
                self._password)
 
835
 
677
836
    def _sftp_open_exclusive(self, abspath, mode=None):
678
837
        """Open a remote path exclusively.
679
838
 
692
851
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
693
852
        #       However, there is no way to set the permission mode at open 
694
853
        #       time using the sftp_client.file() functionality.
695
 
        path = self._get_sftp()._adjust_cwd(abspath)
 
854
        path = self._sftp._adjust_cwd(abspath)
696
855
        # mutter('sftp abspath %s => %s', abspath, path)
697
856
        attr = SFTPAttributes()
698
857
        if mode is not None:
700
859
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
701
860
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
702
861
        try:
703
 
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
 
862
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
704
863
            if t != CMD_HANDLE:
705
864
                raise TransportError('Expected an SFTP handle')
706
865
            handle = msg.get_string()
707
 
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
 
866
            return SFTPFile(self._sftp, handle, 'wb', -1)
708
867
        except (paramiko.SSHException, IOError), e:
709
868
            self._translate_io_exception(e, abspath, ': unable to open',
710
869
                failure_exc=FileExists)
909
1068
        ssh_server.start_server(event, server)
910
1069
        event.wait(5.0)
911
1070
    
912
 
    def setUp(self, backing_server=None):
913
 
        # XXX: TODO: make sftpserver back onto backing_server rather than local
914
 
        # disk.
915
 
        assert (backing_server is None or
916
 
                isinstance(backing_server, local.LocalURLServer)), (
917
 
            "backing_server should not be %r, because this can only serve the "
918
 
            "local current working directory." % (backing_server,))
919
 
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
920
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
 
1071
    def setUp(self):
 
1072
        self._original_vendor = ssh._ssh_vendor
 
1073
        ssh._ssh_vendor = self._vendor
921
1074
        if sys.platform == 'win32':
922
1075
            # Win32 needs to use the UNICODE api
923
1076
            self._homedir = getcwd()
936
1089
    def tearDown(self):
937
1090
        """See bzrlib.transport.Server.tearDown."""
938
1091
        self._listener.stop()
939
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
1092
        ssh._ssh_vendor = self._original_vendor
940
1093
 
941
1094
    def get_bogus_url(self):
942
1095
        """See bzrlib.transport.Server.get_bogus_url."""
953
1106
 
954
1107
    def get_url(self):
955
1108
        """See bzrlib.transport.Server.get_url."""
956
 
        homedir = self._homedir
957
 
        if sys.platform != 'win32':
958
 
            # Remove the initial '/' on all platforms but win32
959
 
            homedir = homedir[1:]
960
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
1109
        return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
961
1110
 
962
1111
 
963
1112
class SFTPServerWithoutSSH(SFTPServer):
995
1144
            else:
996
1145
                raise
997
1146
        except Exception, e:
998
 
            # This typically seems to happen during interpreter shutdown, so
999
 
            # most of the useful ways to report this error are won't work.
1000
 
            # Writing the exception type, and then the text of the exception,
1001
 
            # seems to be the best we can do.
1002
 
            import sys
1003
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
1004
 
            sys.stderr.write('%s\n\n' % (e,))
 
1147
            import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
1005
1148
        server.finish_subsystem()
1006
1149
 
1007
1150
 
1010
1153
 
1011
1154
    def get_url(self):
1012
1155
        """See bzrlib.transport.Server.get_url."""
1013
 
        homedir = self._homedir
1014
 
        if sys.platform != 'win32':
1015
 
            # Remove the initial '/' on all platforms but win32
1016
 
            homedir = homedir[1:]
1017
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
1156
        if sys.platform == 'win32':
 
1157
            return self._get_sftp_url(urlutils.escape(self._homedir))
 
1158
        else:
 
1159
            return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
1018
1160
 
1019
1161
 
1020
1162
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1026
1168
 
1027
1169
 
1028
1170
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1029
 
    """A test server for sftp transports where only absolute paths will work.
1030
 
 
1031
 
    It does this by serving from a deeply-nested directory that doesn't exist.
1032
 
    """
1033
 
 
1034
 
    def setUp(self, backing_server=None):
 
1171
    """A test servere for sftp transports, using absolute urls to non-home."""
 
1172
 
 
1173
    def setUp(self):
1035
1174
        self._server_homedir = '/dev/noone/runs/tests/here'
1036
 
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
 
1175
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
1176
 
 
1177
 
 
1178
def _sftp_connect(host, port, username, password):
 
1179
    """Connect to the remote sftp server.
 
1180
 
 
1181
    :raises: a TransportError 'could not connect'.
 
1182
 
 
1183
    :returns: an paramiko.sftp_client.SFTPClient
 
1184
 
 
1185
    TODO: Raise a more reasonable ConnectionFailed exception
 
1186
    """
 
1187
    idx = (host, port, username)
 
1188
    try:
 
1189
        return _connected_hosts[idx]
 
1190
    except KeyError:
 
1191
        pass
 
1192
    
 
1193
    sftp = _sftp_connect_uncached(host, port, username, password)
 
1194
    _connected_hosts[idx] = sftp
 
1195
    return sftp
 
1196
 
 
1197
def _sftp_connect_uncached(host, port, username, password):
 
1198
    vendor = ssh._get_ssh_vendor()
 
1199
    sftp = vendor.connect_sftp(username, password, host, port)
 
1200
    return sftp
1037
1201
 
1038
1202
 
1039
1203
def get_test_permutations():