~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Andrew Bennetts
  • Date: 2007-03-26 06:24:01 UTC
  • mto: This revision was merged to the branch mainline in revision 2376.
  • Revision ID: andrew.bennetts@canonical.com-20070326062401-k3nbefzje5332jaf
Deal with review comments from Robert:

  * Add my name to the NEWS file
  * Move the test case to a new module in branch_implementations
  * Remove revision_history cruft from identitymap and test_identitymap
  * Improve some docstrings

Also, this fixes a bug where revision_history was not returning a copy of the
cached data, allowing the cache to be corrupted.

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 warnings
 
37
import weakref
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
 
        )
54
51
from bzrlib.trace import mutter, warning
55
52
from bzrlib.transport import (
56
 
    FileFileStream,
57
 
    _file_streams,
58
 
    local,
 
53
    register_urlparse_netloc_protocol,
59
54
    Server,
 
55
    split_url,
60
56
    ssh,
61
 
    ConnectedTransport,
 
57
    Transport,
62
58
    )
63
59
 
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
 
 
77
60
try:
78
61
    import paramiko
79
62
except ImportError, e:
86
69
    from paramiko.sftp_file import SFTPFile
87
70
 
88
71
 
 
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
 
89
82
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
90
83
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
91
84
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
92
85
 
93
86
 
 
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
 
94
95
class SFTPLock(object):
95
96
    """This fakes a lock in a remote location.
96
97
    
102
103
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
103
104
 
104
105
    def __init__(self, path, transport):
 
106
        assert isinstance(transport, SFTPTransport)
 
107
 
105
108
        self.lock_file = None
106
109
        self.path = path
107
110
        self.lock_path = path + '.write-lock'
131
134
            pass
132
135
 
133
136
 
134
 
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):
135
200
    """Transport implementation for SFTP access."""
136
201
 
137
202
    _do_prefetch = _default_do_prefetch
152
217
    # up the request itself, rather than us having to worry about it
153
218
    _max_request_size = 32768
154
219
 
155
 
    def __init__(self, base, _from_transport=None):
156
 
        super(SFTPTransport, self).__init__(base,
157
 
                                            _from_transport=_from_transport)
 
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)
158
245
 
159
246
    def _remote_path(self, relpath):
160
247
        """Return the path to be passed along the sftp protocol for relpath.
161
248
        
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
 
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('/'))
191
310
        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
 
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('/')
207
371
 
208
372
    def has(self, relpath):
209
373
        """
210
374
        Does the target location exist?
211
375
        """
212
376
        try:
213
 
            self._get_sftp().stat(self._remote_path(relpath))
 
377
            self._sftp.stat(self._remote_path(relpath))
214
378
            return True
215
379
        except IOError:
216
380
            return False
223
387
        """
224
388
        try:
225
389
            path = self._remote_path(relpath)
226
 
            f = self._get_sftp().file(path, mode='rb')
 
390
            f = self._sftp.file(path, mode='rb')
227
391
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
228
392
                f.prefetch()
229
393
            return f
230
394
        except (IOError, paramiko.SSHException), e:
231
 
            self._translate_io_exception(e, path, ': error retrieving',
232
 
                failure_exc=errors.ReadError)
 
395
            self._translate_io_exception(e, path, ': error retrieving')
233
396
 
234
 
    def _readv(self, relpath, offsets):
 
397
    def readv(self, relpath, offsets):
235
398
        """See Transport.readv()"""
236
399
        # We overload the default readv() because we want to use a file
237
400
        # that does not have prefetch enabled.
241
404
 
242
405
        try:
243
406
            path = self._remote_path(relpath)
244
 
            fp = self._get_sftp().file(path, mode='rb')
 
407
            fp = self._sftp.file(path, mode='rb')
245
408
            readv = getattr(fp, 'readv', None)
246
409
            if readv:
247
410
                return self._sftp_readv(fp, offsets, relpath)
250
413
        except (IOError, paramiko.SSHException), e:
251
414
            self._translate_io_exception(e, path, ': error retrieving')
252
415
 
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
 
 
261
416
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
262
417
        """Use the readv() member of fp to do async readv.
263
418
 
334
489
 
335
490
            if cur_data_len < cur_coalesced.length:
336
491
                continue
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))
 
492
            assert cur_data_len == cur_coalesced.length, \
 
493
                "Somehow we read too much: %s != %s" % (cur_data_len,
 
494
                                                        cur_coalesced.length)
341
495
            all_data = ''.join(cur_data)
342
496
            cur_data = []
343
497
            cur_data_len = 0
371
525
        :param mode: The final mode for the file
372
526
        """
373
527
        final_path = self._remote_path(relpath)
374
 
        return self._put(final_path, f, mode=mode)
 
528
        self._put(final_path, f, mode=mode)
375
529
 
376
530
    def _put(self, abspath, f, mode=None):
377
531
        """Helper function so both put() and copy_abspaths can reuse the code"""
382
536
        try:
383
537
            try:
384
538
                fout.set_pipelined(True)
385
 
                length = self._pump(f, fout)
 
539
                self._pump(f, fout)
386
540
            except (IOError, paramiko.SSHException), e:
387
541
                self._translate_io_exception(e, tmp_abspath)
388
542
            # XXX: This doesn't truly help like we would like it to.
399
553
            # Because we set_pipelined() earlier, theoretically we might 
400
554
            # avoid the round trip for fout.close()
401
555
            if mode is not None:
402
 
                self._get_sftp().chmod(tmp_abspath, mode)
 
556
                self._sftp.chmod(tmp_abspath, mode)
403
557
            fout.close()
404
558
            closed = True
405
559
            self._rename_and_overwrite(tmp_abspath, abspath)
406
 
            return length
407
560
        except Exception, e:
408
561
            # If we fail, try to clean up the temporary file
409
562
            # before we throw the exception
415
568
            try:
416
569
                if not closed:
417
570
                    fout.close()
418
 
                self._get_sftp().remove(tmp_abspath)
 
571
                self._sftp.remove(tmp_abspath)
419
572
            except:
420
573
                # raise the saved except
421
574
                raise e
436
589
            fout = None
437
590
            try:
438
591
                try:
439
 
                    fout = self._get_sftp().file(abspath, mode='wb')
 
592
                    fout = self._sftp.file(abspath, mode='wb')
440
593
                    fout.set_pipelined(True)
441
594
                    writer(fout)
442
595
                except (paramiko.SSHException, IOError), e:
447
600
                # Because we set_pipelined() earlier, theoretically we might 
448
601
                # avoid the round trip for fout.close()
449
602
                if mode is not None:
450
 
                    self._get_sftp().chmod(abspath, mode)
 
603
                    self._sftp.chmod(abspath, mode)
451
604
            finally:
452
605
                if fout is not None:
453
606
                    fout.close()
517
670
        else:
518
671
            local_mode = mode
519
672
        try:
520
 
            self._get_sftp().mkdir(abspath, local_mode)
 
673
            self._sftp.mkdir(abspath, local_mode)
521
674
            if mode is not None:
522
 
                # chmod a dir through sftp will erase any sgid bit set
523
 
                # on the server side.  So, if the bit mode are already
524
 
                # set, avoid the chmod.  If the mode is not fine but
525
 
                # the sgid bit is set, report a warning to the user
526
 
                # with the umask fix.
527
 
                stat = self._get_sftp().lstat(abspath)
528
 
                mode = mode & 0777 # can't set special bits anyway
529
 
                if mode != stat.st_mode & 0777:
530
 
                    if stat.st_mode & 06000:
531
 
                        warning('About to chmod %s over sftp, which will result'
532
 
                                ' in its suid or sgid bits being cleared.  If'
533
 
                                ' you want to preserve those bits, change your '
534
 
                                ' environment on the server to use umask 0%03o.'
535
 
                                % (abspath, 0777 - mode))
536
 
                    self._get_sftp().chmod(abspath, mode=mode)
 
675
                self._sftp.chmod(abspath, mode=mode)
537
676
        except (paramiko.SSHException, IOError), e:
538
677
            self._translate_io_exception(e, abspath, ': unable to mkdir',
539
678
                failure_exc=FileExists)
542
681
        """Create a directory at the given path."""
543
682
        self._mkdir(self._remote_path(relpath), mode=mode)
544
683
 
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
 
 
567
 
    def _translate_io_exception(self, e, path, more_info='',
 
684
    def _translate_io_exception(self, e, path, more_info='', 
568
685
                                failure_exc=PathError):
569
686
        """Translate a paramiko or IOError into a friendlier exception.
570
687
 
601
718
        """
602
719
        try:
603
720
            path = self._remote_path(relpath)
604
 
            fout = self._get_sftp().file(path, 'ab')
 
721
            fout = self._sftp.file(path, 'ab')
605
722
            if mode is not None:
606
 
                self._get_sftp().chmod(path, mode)
 
723
                self._sftp.chmod(path, mode)
607
724
            result = fout.tell()
608
725
            self._pump(f, fout)
609
726
            return result
613
730
    def rename(self, rel_from, rel_to):
614
731
        """Rename without special overwriting"""
615
732
        try:
616
 
            self._get_sftp().rename(self._remote_path(rel_from),
 
733
            self._sftp.rename(self._remote_path(rel_from),
617
734
                              self._remote_path(rel_to))
618
735
        except (IOError, paramiko.SSHException), e:
619
736
            self._translate_io_exception(e, rel_from,
625
742
        Using the implementation provided by osutils.
626
743
        """
627
744
        try:
628
 
            sftp = self._get_sftp()
629
745
            fancy_rename(abs_from, abs_to,
630
 
                         rename_func=sftp.rename,
631
 
                         unlink_func=sftp.remove)
 
746
                    rename_func=self._sftp.rename,
 
747
                    unlink_func=self._sftp.remove)
632
748
        except (IOError, paramiko.SSHException), e:
633
 
            self._translate_io_exception(e, abs_from,
634
 
                                         ': unable to rename to %r' % (abs_to))
 
749
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
635
750
 
636
751
    def move(self, rel_from, rel_to):
637
752
        """Move the item at rel_from to the location at rel_to"""
643
758
        """Delete the item at relpath"""
644
759
        path = self._remote_path(relpath)
645
760
        try:
646
 
            self._get_sftp().remove(path)
 
761
            self._sftp.remove(path)
647
762
        except (IOError, paramiko.SSHException), e:
648
763
            self._translate_io_exception(e, path, ': unable to delete')
649
764
            
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
 
 
655
765
    def listable(self):
656
766
        """Return True if this store supports listing."""
657
767
        return True
666
776
        # -- David Allouche 2006-08-11
667
777
        path = self._remote_path(relpath)
668
778
        try:
669
 
            entries = self._get_sftp().listdir(path)
 
779
            entries = self._sftp.listdir(path)
670
780
        except (IOError, paramiko.SSHException), e:
671
781
            self._translate_io_exception(e, path, ': failed to list_dir')
672
782
        return [urlutils.escape(entry) for entry in entries]
675
785
        """See Transport.rmdir."""
676
786
        path = self._remote_path(relpath)
677
787
        try:
678
 
            return self._get_sftp().rmdir(path)
 
788
            return self._sftp.rmdir(path)
679
789
        except (IOError, paramiko.SSHException), e:
680
790
            self._translate_io_exception(e, path, ': failed to rmdir')
681
791
 
683
793
        """Return the stat information for a file."""
684
794
        path = self._remote_path(relpath)
685
795
        try:
686
 
            return self._get_sftp().stat(path)
 
796
            return self._sftp.stat(path)
687
797
        except (IOError, paramiko.SSHException), e:
688
798
            self._translate_io_exception(e, path, ': unable to stat')
689
799
 
713
823
        # that we have taken the lock.
714
824
        return SFTPLock(relpath, self)
715
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
 
716
836
    def _sftp_open_exclusive(self, abspath, mode=None):
717
837
        """Open a remote path exclusively.
718
838
 
731
851
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
732
852
        #       However, there is no way to set the permission mode at open 
733
853
        #       time using the sftp_client.file() functionality.
734
 
        path = self._get_sftp()._adjust_cwd(abspath)
 
854
        path = self._sftp._adjust_cwd(abspath)
735
855
        # mutter('sftp abspath %s => %s', abspath, path)
736
856
        attr = SFTPAttributes()
737
857
        if mode is not None:
739
859
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
740
860
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
741
861
        try:
742
 
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
 
862
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
743
863
            if t != CMD_HANDLE:
744
864
                raise TransportError('Expected an SFTP handle')
745
865
            handle = msg.get_string()
746
 
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
 
866
            return SFTPFile(self._sftp, handle, 'wb', -1)
747
867
        except (paramiko.SSHException, IOError), e:
748
868
            self._translate_io_exception(e, abspath, ': unable to open',
749
869
                failure_exc=FileExists)
948
1068
        ssh_server.start_server(event, server)
949
1069
        event.wait(5.0)
950
1070
    
951
 
    def setUp(self, backing_server=None):
952
 
        # XXX: TODO: make sftpserver back onto backing_server rather than local
953
 
        # disk.
954
 
        if not (backing_server is None or
955
 
                isinstance(backing_server, local.LocalURLServer)):
956
 
            raise AssertionError(
957
 
                "backing_server should not be %r, because this can only serve the "
958
 
                "local current working directory." % (backing_server,))
 
1071
    def setUp(self):
959
1072
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
960
1073
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
961
1074
        if sys.platform == 'win32':
1023
1136
            def close(self):
1024
1137
                pass
1025
1138
 
1026
 
        server = paramiko.SFTPServer(
1027
 
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1028
 
            root=self._root, home=self._server_homedir)
 
1139
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
1140
                                     root=self._root, home=self._server_homedir)
1029
1141
        try:
1030
 
            server.start_subsystem(
1031
 
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
 
1142
            server.start_subsystem('sftp', None, sock)
1032
1143
        except socket.error, e:
1033
1144
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
1034
1145
                # it's okay for the client to disconnect abruptly
1037
1148
            else:
1038
1149
                raise
1039
1150
        except Exception, e:
1040
 
            # This typically seems to happen during interpreter shutdown, so
1041
 
            # most of the useful ways to report this error are won't work.
1042
 
            # Writing the exception type, and then the text of the exception,
1043
 
            # seems to be the best we can do.
1044
 
            import sys
1045
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
1046
 
            sys.stderr.write('%s\n\n' % (e,))
 
1151
            import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
1047
1152
        server.finish_subsystem()
1048
1153
 
1049
1154
 
1068
1173
 
1069
1174
 
1070
1175
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1071
 
    """A test server for sftp transports where only absolute paths will work.
1072
 
 
1073
 
    It does this by serving from a deeply-nested directory that doesn't exist.
1074
 
    """
1075
 
 
1076
 
    def setUp(self, backing_server=None):
 
1176
    """A test servere for sftp transports, using absolute urls to non-home."""
 
1177
 
 
1178
    def setUp(self):
1077
1179
        self._server_homedir = '/dev/noone/runs/tests/here'
1078
 
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
 
1180
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
1181
 
 
1182
 
 
1183
def _sftp_connect(host, port, username, password):
 
1184
    """Connect to the remote sftp server.
 
1185
 
 
1186
    :raises: a TransportError 'could not connect'.
 
1187
 
 
1188
    :returns: an paramiko.sftp_client.SFTPClient
 
1189
 
 
1190
    TODO: Raise a more reasonable ConnectionFailed exception
 
1191
    """
 
1192
    idx = (host, port, username)
 
1193
    try:
 
1194
        return _connected_hosts[idx]
 
1195
    except KeyError:
 
1196
        pass
 
1197
    
 
1198
    sftp = _sftp_connect_uncached(host, port, username, password)
 
1199
    _connected_hosts[idx] = sftp
 
1200
    return sftp
 
1201
 
 
1202
def _sftp_connect_uncached(host, port, username, password):
 
1203
    vendor = ssh._get_ssh_vendor()
 
1204
    sftp = vendor.connect_sftp(username, password, host, port)
 
1205
    return sftp
1079
1206
 
1080
1207
 
1081
1208
def get_test_permutations():