~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Michael Hudson
  • Date: 2008-07-17 00:43:32 UTC
  • mto: This revision was merged to the branch mainline in revision 3552.
  • Revision ID: michael.hudson@canonical.com-20080717004332-aqxut6zkoi0hiyl7
the two character fix

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 Canonical Ltd
 
2
# Copyright (C) 2005, 2006, 2007 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
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
        )
51
54
from bzrlib.trace import mutter, warning
52
55
from bzrlib.transport import (
53
 
    register_urlparse_netloc_protocol,
 
56
    FileFileStream,
 
57
    _file_streams,
 
58
    local,
54
59
    Server,
55
 
    split_url,
56
60
    ssh,
57
 
    Transport,
 
61
    ConnectedTransport,
58
62
    )
59
63
 
 
64
# Disable one particular warning that comes from paramiko in Python2.5; if
 
65
# this is emitted at the wrong time it tends to cause spurious test failures
 
66
# or at least noise in the test case::
 
67
#
 
68
# [1770/7639 in 86s, 1 known failures, 50 skipped, 2 missing features]
 
69
# test_permissions.TestSftpPermissions.test_new_files
 
70
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
 
71
#  self.packet.write(struct.pack('>I', n))
 
72
warnings.filterwarnings('ignore',
 
73
        'integer argument expected, got float',
 
74
        category=DeprecationWarning,
 
75
        module='paramiko.message')
 
76
 
60
77
try:
61
78
    import paramiko
62
79
except ImportError, e:
69
86
    from paramiko.sftp_file import SFTPFile
70
87
 
71
88
 
72
 
register_urlparse_netloc_protocol('sftp')
73
 
 
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
 
 
82
89
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
83
90
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
84
91
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
85
92
 
86
93
 
87
 
def clear_connection_cache():
88
 
    """Remove all hosts from the SFTP connection cache.
89
 
 
90
 
    Primarily useful for test cases wanting to force garbage collection.
91
 
    """
92
 
    _connected_hosts.clear()
93
 
 
94
 
 
95
94
class SFTPLock(object):
96
95
    """This fakes a lock in a remote location.
97
96
    
103
102
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
104
103
 
105
104
    def __init__(self, path, transport):
106
 
        assert isinstance(transport, SFTPTransport)
107
 
 
108
105
        self.lock_file = None
109
106
        self.path = path
110
107
        self.lock_path = path + '.write-lock'
134
131
            pass
135
132
 
136
133
 
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
class SFTPTransport(ConnectedTransport):
200
135
    """Transport implementation for SFTP access."""
201
136
 
202
137
    _do_prefetch = _default_do_prefetch
217
152
    # up the request itself, rather than us having to worry about it
218
153
    _max_request_size = 32768
219
154
 
220
 
    def __init__(self, base, clone_from=None):
221
 
        super(SFTPTransport, self).__init__(base)
222
 
        if clone_from is None:
223
 
            self._sftp_connect()
224
 
        else:
225
 
            # use the same ssh connection, etc
226
 
            self._sftp = clone_from._sftp
227
 
        # super saves 'self.base'
228
 
    
229
 
    def should_cache(self):
230
 
        """
231
 
        Return True if the data pulled across should be cached locally.
232
 
        """
233
 
        return True
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)
 
155
    def __init__(self, base, _from_transport=None):
 
156
        super(SFTPTransport, self).__init__(base,
 
157
                                            _from_transport=_from_transport)
245
158
 
246
159
    def _remote_path(self, relpath):
247
160
        """Return the path to be passed along the sftp protocol for relpath.
248
161
        
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('/'))
 
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
310
191
        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('/')
 
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
371
207
 
372
208
    def has(self, relpath):
373
209
        """
374
210
        Does the target location exist?
375
211
        """
376
212
        try:
377
 
            self._sftp.stat(self._remote_path(relpath))
 
213
            self._get_sftp().stat(self._remote_path(relpath))
378
214
            return True
379
215
        except IOError:
380
216
            return False
387
223
        """
388
224
        try:
389
225
            path = self._remote_path(relpath)
390
 
            f = self._sftp.file(path, mode='rb')
 
226
            f = self._get_sftp().file(path, mode='rb')
391
227
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
392
228
                f.prefetch()
393
229
            return f
394
230
        except (IOError, paramiko.SSHException), e:
395
 
            self._translate_io_exception(e, path, ': error retrieving')
 
231
            self._translate_io_exception(e, path, ': error retrieving',
 
232
                failure_exc=errors.ReadError)
396
233
 
397
 
    def readv(self, relpath, offsets):
 
234
    def _readv(self, relpath, offsets):
398
235
        """See Transport.readv()"""
399
236
        # We overload the default readv() because we want to use a file
400
237
        # that does not have prefetch enabled.
404
241
 
405
242
        try:
406
243
            path = self._remote_path(relpath)
407
 
            fp = self._sftp.file(path, mode='rb')
 
244
            fp = self._get_sftp().file(path, mode='rb')
408
245
            readv = getattr(fp, 'readv', None)
409
246
            if readv:
410
247
                return self._sftp_readv(fp, offsets, relpath)
413
250
        except (IOError, paramiko.SSHException), e:
414
251
            self._translate_io_exception(e, path, ': error retrieving')
415
252
 
 
253
    def recommended_page_size(self):
 
254
        """See Transport.recommended_page_size().
 
255
 
 
256
        For SFTP we suggest a large page size to reduce the overhead
 
257
        introduced by latency.
 
258
        """
 
259
        return 64 * 1024
 
260
 
416
261
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
417
262
        """Use the readv() member of fp to do async readv.
418
263
 
489
334
 
490
335
            if cur_data_len < cur_coalesced.length:
491
336
                continue
492
 
            assert cur_data_len == cur_coalesced.length, \
493
 
                "Somehow we read too much: %s != %s" % (cur_data_len,
494
 
                                                        cur_coalesced.length)
 
337
            if cur_data_len != cur_coalesced.length:
 
338
                raise AssertionError(
 
339
                    "Somehow we read too much: %s != %s" 
 
340
                    % (cur_data_len, cur_coalesced.length))
495
341
            all_data = ''.join(cur_data)
496
342
            cur_data = []
497
343
            cur_data_len = 0
525
371
        :param mode: The final mode for the file
526
372
        """
527
373
        final_path = self._remote_path(relpath)
528
 
        self._put(final_path, f, mode=mode)
 
374
        return self._put(final_path, f, mode=mode)
529
375
 
530
376
    def _put(self, abspath, f, mode=None):
531
377
        """Helper function so both put() and copy_abspaths can reuse the code"""
536
382
        try:
537
383
            try:
538
384
                fout.set_pipelined(True)
539
 
                self._pump(f, fout)
 
385
                length = self._pump(f, fout)
540
386
            except (IOError, paramiko.SSHException), e:
541
387
                self._translate_io_exception(e, tmp_abspath)
542
388
            # XXX: This doesn't truly help like we would like it to.
553
399
            # Because we set_pipelined() earlier, theoretically we might 
554
400
            # avoid the round trip for fout.close()
555
401
            if mode is not None:
556
 
                self._sftp.chmod(tmp_abspath, mode)
 
402
                self._get_sftp().chmod(tmp_abspath, mode)
557
403
            fout.close()
558
404
            closed = True
559
405
            self._rename_and_overwrite(tmp_abspath, abspath)
 
406
            return length
560
407
        except Exception, e:
561
408
            # If we fail, try to clean up the temporary file
562
409
            # before we throw the exception
568
415
            try:
569
416
                if not closed:
570
417
                    fout.close()
571
 
                self._sftp.remove(tmp_abspath)
 
418
                self._get_sftp().remove(tmp_abspath)
572
419
            except:
573
420
                # raise the saved except
574
421
                raise e
589
436
            fout = None
590
437
            try:
591
438
                try:
592
 
                    fout = self._sftp.file(abspath, mode='wb')
 
439
                    fout = self._get_sftp().file(abspath, mode='wb')
593
440
                    fout.set_pipelined(True)
594
441
                    writer(fout)
595
442
                except (paramiko.SSHException, IOError), e:
600
447
                # Because we set_pipelined() earlier, theoretically we might 
601
448
                # avoid the round trip for fout.close()
602
449
                if mode is not None:
603
 
                    self._sftp.chmod(abspath, mode)
 
450
                    self._get_sftp().chmod(abspath, mode)
604
451
            finally:
605
452
                if fout is not None:
606
453
                    fout.close()
670
517
        else:
671
518
            local_mode = mode
672
519
        try:
673
 
            self._sftp.mkdir(abspath, local_mode)
 
520
            self._get_sftp().mkdir(abspath, local_mode)
674
521
            if mode is not None:
675
 
                self._sftp.chmod(abspath, mode=mode)
 
522
                self._get_sftp().chmod(abspath, mode=mode)
676
523
        except (paramiko.SSHException, IOError), e:
677
524
            self._translate_io_exception(e, abspath, ': unable to mkdir',
678
525
                failure_exc=FileExists)
681
528
        """Create a directory at the given path."""
682
529
        self._mkdir(self._remote_path(relpath), mode=mode)
683
530
 
684
 
    def _translate_io_exception(self, e, path, more_info='', 
 
531
    def open_write_stream(self, relpath, mode=None):
 
532
        """See Transport.open_write_stream."""
 
533
        # initialise the file to zero-length
 
534
        # this is three round trips, but we don't use this 
 
535
        # api more than once per write_group at the moment so 
 
536
        # it is a tolerable overhead. Better would be to truncate
 
537
        # the file after opening. RBC 20070805
 
538
        self.put_bytes_non_atomic(relpath, "", mode)
 
539
        abspath = self._remote_path(relpath)
 
540
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
 
541
        #       set the file mode at create time. If it does, use it.
 
542
        #       But for now, we just chmod later anyway.
 
543
        handle = None
 
544
        try:
 
545
            handle = self._get_sftp().file(abspath, mode='wb')
 
546
            handle.set_pipelined(True)
 
547
        except (paramiko.SSHException, IOError), e:
 
548
            self._translate_io_exception(e, abspath,
 
549
                                         ': unable to open')
 
550
        _file_streams[self.abspath(relpath)] = handle
 
551
        return FileFileStream(self, relpath, handle)
 
552
 
 
553
    def _translate_io_exception(self, e, path, more_info='',
685
554
                                failure_exc=PathError):
686
555
        """Translate a paramiko or IOError into a friendlier exception.
687
556
 
718
587
        """
719
588
        try:
720
589
            path = self._remote_path(relpath)
721
 
            fout = self._sftp.file(path, 'ab')
 
590
            fout = self._get_sftp().file(path, 'ab')
722
591
            if mode is not None:
723
 
                self._sftp.chmod(path, mode)
 
592
                self._get_sftp().chmod(path, mode)
724
593
            result = fout.tell()
725
594
            self._pump(f, fout)
726
595
            return result
730
599
    def rename(self, rel_from, rel_to):
731
600
        """Rename without special overwriting"""
732
601
        try:
733
 
            self._sftp.rename(self._remote_path(rel_from),
 
602
            self._get_sftp().rename(self._remote_path(rel_from),
734
603
                              self._remote_path(rel_to))
735
604
        except (IOError, paramiko.SSHException), e:
736
605
            self._translate_io_exception(e, rel_from,
742
611
        Using the implementation provided by osutils.
743
612
        """
744
613
        try:
 
614
            sftp = self._get_sftp()
745
615
            fancy_rename(abs_from, abs_to,
746
 
                    rename_func=self._sftp.rename,
747
 
                    unlink_func=self._sftp.remove)
 
616
                         rename_func=sftp.rename,
 
617
                         unlink_func=sftp.remove)
748
618
        except (IOError, paramiko.SSHException), e:
749
 
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
 
619
            self._translate_io_exception(e, abs_from,
 
620
                                         ': unable to rename to %r' % (abs_to))
750
621
 
751
622
    def move(self, rel_from, rel_to):
752
623
        """Move the item at rel_from to the location at rel_to"""
758
629
        """Delete the item at relpath"""
759
630
        path = self._remote_path(relpath)
760
631
        try:
761
 
            self._sftp.remove(path)
 
632
            self._get_sftp().remove(path)
762
633
        except (IOError, paramiko.SSHException), e:
763
634
            self._translate_io_exception(e, path, ': unable to delete')
764
635
            
 
636
    def external_url(self):
 
637
        """See bzrlib.transport.Transport.external_url."""
 
638
        # the external path for SFTP is the base
 
639
        return self.base
 
640
 
765
641
    def listable(self):
766
642
        """Return True if this store supports listing."""
767
643
        return True
776
652
        # -- David Allouche 2006-08-11
777
653
        path = self._remote_path(relpath)
778
654
        try:
779
 
            entries = self._sftp.listdir(path)
 
655
            entries = self._get_sftp().listdir(path)
780
656
        except (IOError, paramiko.SSHException), e:
781
657
            self._translate_io_exception(e, path, ': failed to list_dir')
782
658
        return [urlutils.escape(entry) for entry in entries]
785
661
        """See Transport.rmdir."""
786
662
        path = self._remote_path(relpath)
787
663
        try:
788
 
            return self._sftp.rmdir(path)
 
664
            return self._get_sftp().rmdir(path)
789
665
        except (IOError, paramiko.SSHException), e:
790
666
            self._translate_io_exception(e, path, ': failed to rmdir')
791
667
 
793
669
        """Return the stat information for a file."""
794
670
        path = self._remote_path(relpath)
795
671
        try:
796
 
            return self._sftp.stat(path)
 
672
            return self._get_sftp().stat(path)
797
673
        except (IOError, paramiko.SSHException), e:
798
674
            self._translate_io_exception(e, path, ': unable to stat')
799
675
 
823
699
        # that we have taken the lock.
824
700
        return SFTPLock(relpath, self)
825
701
 
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
 
 
836
702
    def _sftp_open_exclusive(self, abspath, mode=None):
837
703
        """Open a remote path exclusively.
838
704
 
851
717
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
852
718
        #       However, there is no way to set the permission mode at open 
853
719
        #       time using the sftp_client.file() functionality.
854
 
        path = self._sftp._adjust_cwd(abspath)
 
720
        path = self._get_sftp()._adjust_cwd(abspath)
855
721
        # mutter('sftp abspath %s => %s', abspath, path)
856
722
        attr = SFTPAttributes()
857
723
        if mode is not None:
859
725
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
860
726
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
861
727
        try:
862
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
 
728
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
863
729
            if t != CMD_HANDLE:
864
730
                raise TransportError('Expected an SFTP handle')
865
731
            handle = msg.get_string()
866
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
 
732
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
867
733
        except (paramiko.SSHException, IOError), e:
868
734
            self._translate_io_exception(e, abspath, ': unable to open',
869
735
                failure_exc=FileExists)
1068
934
        ssh_server.start_server(event, server)
1069
935
        event.wait(5.0)
1070
936
    
1071
 
    def setUp(self):
1072
 
        self._original_vendor = ssh._ssh_vendor
1073
 
        ssh._ssh_vendor = self._vendor
 
937
    def setUp(self, backing_server=None):
 
938
        # XXX: TODO: make sftpserver back onto backing_server rather than local
 
939
        # disk.
 
940
        if not (backing_server is None or
 
941
                isinstance(backing_server, local.LocalURLServer)):
 
942
            raise AssertionError(
 
943
                "backing_server should not be %r, because this can only serve the "
 
944
                "local current working directory." % (backing_server,))
 
945
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
 
946
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
1074
947
        if sys.platform == 'win32':
1075
948
            # Win32 needs to use the UNICODE api
1076
949
            self._homedir = getcwd()
1089
962
    def tearDown(self):
1090
963
        """See bzrlib.transport.Server.tearDown."""
1091
964
        self._listener.stop()
1092
 
        ssh._ssh_vendor = self._original_vendor
 
965
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
1093
966
 
1094
967
    def get_bogus_url(self):
1095
968
        """See bzrlib.transport.Server.get_bogus_url."""
1106
979
 
1107
980
    def get_url(self):
1108
981
        """See bzrlib.transport.Server.get_url."""
1109
 
        return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
 
982
        homedir = self._homedir
 
983
        if sys.platform != 'win32':
 
984
            # Remove the initial '/' on all platforms but win32
 
985
            homedir = homedir[1:]
 
986
        return self._get_sftp_url(urlutils.escape(homedir))
1110
987
 
1111
988
 
1112
989
class SFTPServerWithoutSSH(SFTPServer):
1132
1009
            def close(self):
1133
1010
                pass
1134
1011
 
1135
 
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1136
 
                                     root=self._root, home=self._server_homedir)
 
1012
        server = paramiko.SFTPServer(
 
1013
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
1014
            root=self._root, home=self._server_homedir)
1137
1015
        try:
1138
 
            server.start_subsystem('sftp', None, sock)
 
1016
            server.start_subsystem(
 
1017
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
1139
1018
        except socket.error, e:
1140
1019
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
1141
1020
                # it's okay for the client to disconnect abruptly
1144
1023
            else:
1145
1024
                raise
1146
1025
        except Exception, e:
1147
 
            import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
 
1026
            # This typically seems to happen during interpreter shutdown, so
 
1027
            # most of the useful ways to report this error are won't work.
 
1028
            # Writing the exception type, and then the text of the exception,
 
1029
            # seems to be the best we can do.
 
1030
            import sys
 
1031
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
1032
            sys.stderr.write('%s\n\n' % (e,))
1148
1033
        server.finish_subsystem()
1149
1034
 
1150
1035
 
1153
1038
 
1154
1039
    def get_url(self):
1155
1040
        """See bzrlib.transport.Server.get_url."""
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:]))
 
1041
        homedir = self._homedir
 
1042
        if sys.platform != 'win32':
 
1043
            # Remove the initial '/' on all platforms but win32
 
1044
            homedir = homedir[1:]
 
1045
        return self._get_sftp_url(urlutils.escape(homedir))
1160
1046
 
1161
1047
 
1162
1048
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1168
1054
 
1169
1055
 
1170
1056
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1171
 
    """A test servere for sftp transports, using absolute urls to non-home."""
1172
 
 
1173
 
    def setUp(self):
 
1057
    """A test server for sftp transports where only absolute paths will work.
 
1058
 
 
1059
    It does this by serving from a deeply-nested directory that doesn't exist.
 
1060
    """
 
1061
 
 
1062
    def setUp(self, backing_server=None):
1174
1063
        self._server_homedir = '/dev/noone/runs/tests/here'
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
 
1064
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
1201
1065
 
1202
1066
 
1203
1067
def get_test_permutations():