/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

First attempt to merge .dev and resolve the conflicts (but tests are 
failing)

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
30
30
import select
31
31
import socket
32
32
import stat
33
 
import subprocess
34
33
import sys
35
34
import time
36
35
import urllib
37
36
import urlparse
38
 
import weakref
 
37
import warnings
39
38
 
40
 
from bzrlib.errors import (FileExists, 
 
39
from bzrlib import (
 
40
    errors,
 
41
    urlutils,
 
42
    )
 
43
from bzrlib.errors import (FileExists,
41
44
                           NoSuchFile, PathNotChild,
42
45
                           TransportError,
43
 
                           LockError, 
 
46
                           LockError,
44
47
                           PathError,
45
48
                           ParamikoNotPresent,
46
 
                           UnknownSSH,
47
49
                           )
48
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
 
51
from bzrlib.symbol_versioning import (
 
52
        deprecated_function,
 
53
        )
49
54
from bzrlib.trace import mutter, warning
50
55
from bzrlib.transport import (
51
 
    register_urlparse_netloc_protocol,
 
56
    FileFileStream,
 
57
    _file_streams,
 
58
    local,
52
59
    Server,
53
 
    split_url,
54
60
    ssh,
55
 
    Transport,
 
61
    ConnectedTransport,
56
62
    )
57
 
import bzrlib.urlutils as urlutils
 
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')
58
76
 
59
77
try:
60
78
    import paramiko
68
86
    from paramiko.sftp_file import SFTPFile
69
87
 
70
88
 
71
 
register_urlparse_netloc_protocol('sftp')
72
 
 
73
 
 
74
 
# This is a weakref dictionary, so that we can reuse connections
75
 
# that are still active. Long term, it might be nice to have some
76
 
# sort of expiration policy, such as disconnect if inactive for
77
 
# X seconds. But that requires a lot more fanciness.
78
 
_connected_hosts = weakref.WeakValueDictionary()
79
 
 
80
 
 
81
89
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
82
90
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
83
91
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
84
92
 
85
93
 
86
 
def clear_connection_cache():
87
 
    """Remove all hosts from the SFTP connection cache.
88
 
 
89
 
    Primarily useful for test cases wanting to force garbage collection.
90
 
    """
91
 
    _connected_hosts.clear()
92
 
 
93
 
 
94
94
class SFTPLock(object):
95
95
    """This fakes a lock in a remote location.
96
96
    
102
102
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
103
103
 
104
104
    def __init__(self, path, transport):
105
 
        assert isinstance(transport, SFTPTransport)
106
 
 
107
105
        self.lock_file = None
108
106
        self.path = path
109
107
        self.lock_path = path + '.write-lock'
133
131
            pass
134
132
 
135
133
 
136
 
class SFTPTransport(Transport):
 
134
class SFTPTransport(ConnectedTransport):
137
135
    """Transport implementation for SFTP access."""
138
136
 
139
137
    _do_prefetch = _default_do_prefetch
154
152
    # up the request itself, rather than us having to worry about it
155
153
    _max_request_size = 32768
156
154
 
157
 
    def __init__(self, base, clone_from=None):
158
 
        assert base.startswith('sftp://')
159
 
        self._parse_url(base)
160
 
        base = self._unparse_url()
161
 
        if base[-1] != '/':
162
 
            base += '/'
163
 
        super(SFTPTransport, self).__init__(base)
164
 
        if clone_from is None:
165
 
            self._sftp_connect()
166
 
        else:
167
 
            # use the same ssh connection, etc
168
 
            self._sftp = clone_from._sftp
169
 
        # super saves 'self.base'
170
 
    
171
 
    def should_cache(self):
172
 
        """
173
 
        Return True if the data pulled across should be cached locally.
174
 
        """
175
 
        return True
176
 
 
177
 
    def clone(self, offset=None):
178
 
        """
179
 
        Return a new SFTPTransport with root at self.base + offset.
180
 
        We share the same SFTP session between such transports, because it's
181
 
        fairly expensive to set them up.
182
 
        """
183
 
        if offset is None:
184
 
            return SFTPTransport(self.base, self)
185
 
        else:
186
 
            return SFTPTransport(self.abspath(offset), self)
187
 
 
188
 
    def abspath(self, relpath):
189
 
        """
190
 
        Return the full url to the given relative path.
191
 
        
192
 
        @param relpath: the relative path or path components
193
 
        @type relpath: str or list
194
 
        """
195
 
        return self._unparse_url(self._remote_path(relpath))
196
 
    
 
155
    def __init__(self, base, _from_transport=None):
 
156
        super(SFTPTransport, self).__init__(base,
 
157
                                            _from_transport=_from_transport)
 
158
 
197
159
    def _remote_path(self, relpath):
198
160
        """Return the path to be passed along the sftp protocol for relpath.
199
161
        
200
 
        relpath is a urlencoded string.
201
 
        """
202
 
        # FIXME: share the common code across transports
203
 
        assert isinstance(relpath, basestring)
204
 
        relpath = urlutils.unescape(relpath).split('/')
205
 
        basepath = self._path.split('/')
206
 
        if len(basepath) > 0 and basepath[-1] == '':
207
 
            basepath = basepath[:-1]
208
 
 
209
 
        for p in relpath:
210
 
            if p == '..':
211
 
                if len(basepath) == 0:
212
 
                    # In most filesystems, a request for the parent
213
 
                    # of root, just returns root.
214
 
                    continue
215
 
                basepath.pop()
216
 
            elif p == '.':
217
 
                continue # No-op
218
 
            else:
219
 
                basepath.append(p)
220
 
 
221
 
        path = '/'.join(basepath)
222
 
        # mutter('relpath => remotepath %s => %s', relpath, path)
223
 
        return path
224
 
 
225
 
    def relpath(self, abspath):
226
 
        username, password, host, port, path = self._split_url(abspath)
227
 
        error = []
228
 
        if (username != self._username):
229
 
            error.append('username mismatch')
230
 
        if (host != self._host):
231
 
            error.append('host mismatch')
232
 
        if (port != self._port):
233
 
            error.append('port mismatch')
234
 
        if (not path.startswith(self._path)):
235
 
            error.append('path mismatch')
236
 
        if error:
237
 
            extra = ': ' + ', '.join(error)
238
 
            raise PathNotChild(abspath, self.base, extra=extra)
239
 
        pl = len(self._path)
240
 
        return path[pl:].strip('/')
 
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
 
191
        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
241
207
 
242
208
    def has(self, relpath):
243
209
        """
244
210
        Does the target location exist?
245
211
        """
246
212
        try:
247
 
            self._sftp.stat(self._remote_path(relpath))
 
213
            self._get_sftp().stat(self._remote_path(relpath))
248
214
            return True
249
215
        except IOError:
250
216
            return False
257
223
        """
258
224
        try:
259
225
            path = self._remote_path(relpath)
260
 
            f = self._sftp.file(path, mode='rb')
 
226
            f = self._get_sftp().file(path, mode='rb')
261
227
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
262
228
                f.prefetch()
263
229
            return f
264
230
        except (IOError, paramiko.SSHException), e:
265
 
            self._translate_io_exception(e, path, ': error retrieving')
 
231
            self._translate_io_exception(e, path, ': error retrieving',
 
232
                failure_exc=errors.ReadError)
266
233
 
267
 
    def readv(self, relpath, offsets):
 
234
    def _readv(self, relpath, offsets):
268
235
        """See Transport.readv()"""
269
236
        # We overload the default readv() because we want to use a file
270
237
        # that does not have prefetch enabled.
274
241
 
275
242
        try:
276
243
            path = self._remote_path(relpath)
277
 
            fp = self._sftp.file(path, mode='rb')
 
244
            fp = self._get_sftp().file(path, mode='rb')
278
245
            readv = getattr(fp, 'readv', None)
279
246
            if readv:
280
 
                return self._sftp_readv(fp, offsets)
 
247
                return self._sftp_readv(fp, offsets, relpath)
281
248
            mutter('seek and read %s offsets', len(offsets))
282
 
            return self._seek_and_read(fp, offsets)
 
249
            return self._seek_and_read(fp, offsets, relpath)
283
250
        except (IOError, paramiko.SSHException), e:
284
251
            self._translate_io_exception(e, path, ': error retrieving')
285
252
 
286
 
    def _sftp_readv(self, fp, offsets):
 
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
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
287
262
        """Use the readv() member of fp to do async readv.
288
263
 
289
264
        And then read them using paramiko.readv(). paramiko.readv()
359
334
 
360
335
            if cur_data_len < cur_coalesced.length:
361
336
                continue
362
 
            assert cur_data_len == cur_coalesced.length, \
363
 
                "Somehow we read too much: %s != %s" % (cur_data_len,
364
 
                                                        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))
365
341
            all_data = ''.join(cur_data)
366
342
            cur_data = []
367
343
            cur_data_len = 0
376
352
                yield cur_offset_and_size[0], this_data
377
353
                cur_offset_and_size = offset_stack.next()
378
354
 
 
355
            # We read a coalesced entry, so mark it as done
 
356
            cur_coalesced = None
379
357
            # Now that we've read all of the data for this coalesced section
380
358
            # on to the next
381
359
            cur_coalesced = cur_coalesced_stack.next()
382
360
 
 
361
        if cur_coalesced is not None:
 
362
            raise errors.ShortReadvError(relpath, cur_coalesced.start,
 
363
                cur_coalesced.length, len(data))
 
364
 
383
365
    def put_file(self, relpath, f, mode=None):
384
366
        """
385
367
        Copy the file-like object into the location.
389
371
        :param mode: The final mode for the file
390
372
        """
391
373
        final_path = self._remote_path(relpath)
392
 
        self._put(final_path, f, mode=mode)
 
374
        return self._put(final_path, f, mode=mode)
393
375
 
394
376
    def _put(self, abspath, f, mode=None):
395
377
        """Helper function so both put() and copy_abspaths can reuse the code"""
400
382
        try:
401
383
            try:
402
384
                fout.set_pipelined(True)
403
 
                self._pump(f, fout)
 
385
                length = self._pump(f, fout)
404
386
            except (IOError, paramiko.SSHException), e:
405
387
                self._translate_io_exception(e, tmp_abspath)
406
388
            # XXX: This doesn't truly help like we would like it to.
417
399
            # Because we set_pipelined() earlier, theoretically we might 
418
400
            # avoid the round trip for fout.close()
419
401
            if mode is not None:
420
 
                self._sftp.chmod(tmp_abspath, mode)
 
402
                self._get_sftp().chmod(tmp_abspath, mode)
421
403
            fout.close()
422
404
            closed = True
423
405
            self._rename_and_overwrite(tmp_abspath, abspath)
 
406
            return length
424
407
        except Exception, e:
425
408
            # If we fail, try to clean up the temporary file
426
409
            # before we throw the exception
432
415
            try:
433
416
                if not closed:
434
417
                    fout.close()
435
 
                self._sftp.remove(tmp_abspath)
 
418
                self._get_sftp().remove(tmp_abspath)
436
419
            except:
437
420
                # raise the saved except
438
421
                raise e
453
436
            fout = None
454
437
            try:
455
438
                try:
456
 
                    fout = self._sftp.file(abspath, mode='wb')
 
439
                    fout = self._get_sftp().file(abspath, mode='wb')
457
440
                    fout.set_pipelined(True)
458
441
                    writer(fout)
459
442
                except (paramiko.SSHException, IOError), e:
464
447
                # Because we set_pipelined() earlier, theoretically we might 
465
448
                # avoid the round trip for fout.close()
466
449
                if mode is not None:
467
 
                    self._sftp.chmod(abspath, mode)
 
450
                    self._get_sftp().chmod(abspath, mode)
468
451
            finally:
469
452
                if fout is not None:
470
453
                    fout.close()
534
517
        else:
535
518
            local_mode = mode
536
519
        try:
537
 
            self._sftp.mkdir(abspath, local_mode)
 
520
            self._get_sftp().mkdir(abspath, local_mode)
538
521
            if mode is not None:
539
 
                self._sftp.chmod(abspath, mode=mode)
 
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)
540
537
        except (paramiko.SSHException, IOError), e:
541
538
            self._translate_io_exception(e, abspath, ': unable to mkdir',
542
539
                failure_exc=FileExists)
545
542
        """Create a directory at the given path."""
546
543
        self._mkdir(self._remote_path(relpath), mode=mode)
547
544
 
548
 
    def _translate_io_exception(self, e, path, more_info='', 
 
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='',
549
568
                                failure_exc=PathError):
550
569
        """Translate a paramiko or IOError into a friendlier exception.
551
570
 
565
584
            if (e.args == ('No such file or directory',) or
566
585
                e.args == ('No such file',)):
567
586
                raise NoSuchFile(path, str(e) + more_info)
568
 
            if (e.args == ('mkdir failed',)):
 
587
            if (e.args == ('mkdir failed',) or
 
588
                e.args[0].startswith('syserr: File exists')):
569
589
                raise FileExists(path, str(e) + more_info)
570
590
            # strange but true, for the paramiko server.
571
591
            if (e.args == ('Failure',)):
582
602
        """
583
603
        try:
584
604
            path = self._remote_path(relpath)
585
 
            fout = self._sftp.file(path, 'ab')
 
605
            fout = self._get_sftp().file(path, 'ab')
586
606
            if mode is not None:
587
 
                self._sftp.chmod(path, mode)
 
607
                self._get_sftp().chmod(path, mode)
588
608
            result = fout.tell()
589
609
            self._pump(f, fout)
590
610
            return result
594
614
    def rename(self, rel_from, rel_to):
595
615
        """Rename without special overwriting"""
596
616
        try:
597
 
            self._sftp.rename(self._remote_path(rel_from),
 
617
            self._get_sftp().rename(self._remote_path(rel_from),
598
618
                              self._remote_path(rel_to))
599
619
        except (IOError, paramiko.SSHException), e:
600
620
            self._translate_io_exception(e, rel_from,
606
626
        Using the implementation provided by osutils.
607
627
        """
608
628
        try:
 
629
            sftp = self._get_sftp()
609
630
            fancy_rename(abs_from, abs_to,
610
 
                    rename_func=self._sftp.rename,
611
 
                    unlink_func=self._sftp.remove)
 
631
                         rename_func=sftp.rename,
 
632
                         unlink_func=sftp.remove)
612
633
        except (IOError, paramiko.SSHException), e:
613
 
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
 
634
            self._translate_io_exception(e, abs_from,
 
635
                                         ': unable to rename to %r' % (abs_to))
614
636
 
615
637
    def move(self, rel_from, rel_to):
616
638
        """Move the item at rel_from to the location at rel_to"""
622
644
        """Delete the item at relpath"""
623
645
        path = self._remote_path(relpath)
624
646
        try:
625
 
            self._sftp.remove(path)
 
647
            self._get_sftp().remove(path)
626
648
        except (IOError, paramiko.SSHException), e:
627
649
            self._translate_io_exception(e, path, ': unable to delete')
628
650
            
 
651
    def external_url(self):
 
652
        """See bzrlib.transport.Transport.external_url."""
 
653
        # the external path for SFTP is the base
 
654
        return self.base
 
655
 
629
656
    def listable(self):
630
657
        """Return True if this store supports listing."""
631
658
        return True
640
667
        # -- David Allouche 2006-08-11
641
668
        path = self._remote_path(relpath)
642
669
        try:
643
 
            entries = self._sftp.listdir(path)
 
670
            entries = self._get_sftp().listdir(path)
644
671
        except (IOError, paramiko.SSHException), e:
645
672
            self._translate_io_exception(e, path, ': failed to list_dir')
646
673
        return [urlutils.escape(entry) for entry in entries]
649
676
        """See Transport.rmdir."""
650
677
        path = self._remote_path(relpath)
651
678
        try:
652
 
            return self._sftp.rmdir(path)
 
679
            return self._get_sftp().rmdir(path)
653
680
        except (IOError, paramiko.SSHException), e:
654
681
            self._translate_io_exception(e, path, ': failed to rmdir')
655
682
 
657
684
        """Return the stat information for a file."""
658
685
        path = self._remote_path(relpath)
659
686
        try:
660
 
            return self._sftp.stat(path)
 
687
            return self._get_sftp().stat(path)
661
688
        except (IOError, paramiko.SSHException), e:
662
689
            self._translate_io_exception(e, path, ': unable to stat')
663
690
 
687
714
        # that we have taken the lock.
688
715
        return SFTPLock(relpath, self)
689
716
 
690
 
    def _unparse_url(self, path=None):
691
 
        if path is None:
692
 
            path = self._path
693
 
        path = urllib.quote(path)
694
 
        # handle homedir paths
695
 
        if not path.startswith('/'):
696
 
            path = "/~/" + path
697
 
        netloc = urllib.quote(self._host)
698
 
        if self._username is not None:
699
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
700
 
        if self._port is not None:
701
 
            netloc = '%s:%d' % (netloc, self._port)
702
 
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
703
 
 
704
 
    def _split_url(self, url):
705
 
        (scheme, username, password, host, port, path) = split_url(url)
706
 
        assert scheme == 'sftp'
707
 
 
708
 
        # the initial slash should be removed from the path, and treated
709
 
        # as a homedir relative path (the path begins with a double slash
710
 
        # if it is absolute).
711
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
712
 
        # RBC 20060118 we are not using this as its too user hostile. instead
713
 
        # we are following lftp and using /~/foo to mean '~/foo'.
714
 
        # handle homedir paths
715
 
        if path.startswith('/~/'):
716
 
            path = path[3:]
717
 
        elif path == '/~':
718
 
            path = ''
719
 
        return (username, password, host, port, path)
720
 
 
721
 
    def _parse_url(self, url):
722
 
        (self._username, self._password,
723
 
         self._host, self._port, self._path) = self._split_url(url)
724
 
 
725
 
    def _sftp_connect(self):
726
 
        """Connect to the remote sftp server.
727
 
        After this, self._sftp should have a valid connection (or
728
 
        we raise an TransportError 'could not connect').
729
 
 
730
 
        TODO: Raise a more reasonable ConnectionFailed exception
731
 
        """
732
 
        self._sftp = _sftp_connect(self._host, self._port, self._username,
733
 
                self._password)
734
 
 
735
717
    def _sftp_open_exclusive(self, abspath, mode=None):
736
718
        """Open a remote path exclusively.
737
719
 
750
732
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
751
733
        #       However, there is no way to set the permission mode at open 
752
734
        #       time using the sftp_client.file() functionality.
753
 
        path = self._sftp._adjust_cwd(abspath)
 
735
        path = self._get_sftp()._adjust_cwd(abspath)
754
736
        # mutter('sftp abspath %s => %s', abspath, path)
755
737
        attr = SFTPAttributes()
756
738
        if mode is not None:
758
740
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
759
741
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
760
742
        try:
761
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
 
743
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
762
744
            if t != CMD_HANDLE:
763
745
                raise TransportError('Expected an SFTP handle')
764
746
            handle = msg.get_string()
765
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
 
747
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
766
748
        except (paramiko.SSHException, IOError), e:
767
749
            self._translate_io_exception(e, abspath, ': unable to open',
768
750
                failure_exc=FileExists)
921
903
class SFTPServer(Server):
922
904
    """Common code for SFTP server facilities."""
923
905
 
924
 
    def __init__(self):
 
906
    def __init__(self, server_interface=StubServer):
925
907
        self._original_vendor = None
926
908
        self._homedir = None
927
909
        self._server_homedir = None
928
910
        self._listener = None
929
911
        self._root = None
930
912
        self._vendor = ssh.ParamikoVendor()
 
913
        self._server_interface = server_interface
931
914
        # sftp server logs
932
915
        self.logs = []
933
916
        self.add_latency = 0
958
941
        f.close()
959
942
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
960
943
        ssh_server.add_server_key(host_key)
961
 
        server = StubServer(self)
 
944
        server = self._server_interface(self)
962
945
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
963
946
                                         StubSFTPServer, root=self._root,
964
947
                                         home=self._server_homedir)
966
949
        ssh_server.start_server(event, server)
967
950
        event.wait(5.0)
968
951
    
969
 
    def setUp(self):
970
 
        self._original_vendor = ssh._ssh_vendor
971
 
        ssh._ssh_vendor = self._vendor
 
952
    def setUp(self, backing_server=None):
 
953
        # XXX: TODO: make sftpserver back onto backing_server rather than local
 
954
        # disk.
 
955
        if not (backing_server is None or
 
956
                isinstance(backing_server, local.LocalURLServer)):
 
957
            raise AssertionError(
 
958
                "backing_server should not be %r, because this can only serve the "
 
959
                "local current working directory." % (backing_server,))
 
960
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
 
961
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
972
962
        if sys.platform == 'win32':
973
963
            # Win32 needs to use the UNICODE api
974
964
            self._homedir = getcwd()
987
977
    def tearDown(self):
988
978
        """See bzrlib.transport.Server.tearDown."""
989
979
        self._listener.stop()
990
 
        ssh._ssh_vendor = self._original_vendor
 
980
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
991
981
 
992
982
    def get_bogus_url(self):
993
983
        """See bzrlib.transport.Server.get_bogus_url."""
1004
994
 
1005
995
    def get_url(self):
1006
996
        """See bzrlib.transport.Server.get_url."""
1007
 
        return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
 
997
        homedir = self._homedir
 
998
        if sys.platform != 'win32':
 
999
            # Remove the initial '/' on all platforms but win32
 
1000
            homedir = homedir[1:]
 
1001
        return self._get_sftp_url(urlutils.escape(homedir))
1008
1002
 
1009
1003
 
1010
1004
class SFTPServerWithoutSSH(SFTPServer):
1018
1012
        # Re-import these as locals, so that they're still accessible during
1019
1013
        # interpreter shutdown (when all module globals get set to None, leading
1020
1014
        # to confusing errors like "'NoneType' object has no attribute 'error'".
1021
 
        import socket, errno
1022
1015
        class FakeChannel(object):
1023
1016
            def get_transport(self):
1024
1017
                return self
1031
1024
            def close(self):
1032
1025
                pass
1033
1026
 
1034
 
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1035
 
                                     root=self._root, home=self._server_homedir)
 
1027
        server = paramiko.SFTPServer(
 
1028
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
1029
            root=self._root, home=self._server_homedir)
1036
1030
        try:
1037
 
            server.start_subsystem('sftp', None, sock)
 
1031
            server.start_subsystem(
 
1032
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
1038
1033
        except socket.error, e:
1039
1034
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
1040
1035
                # it's okay for the client to disconnect abruptly
1043
1038
            else:
1044
1039
                raise
1045
1040
        except Exception, e:
1046
 
            import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
 
1041
            # This typically seems to happen during interpreter shutdown, so
 
1042
            # most of the useful ways to report this error are won't work.
 
1043
            # Writing the exception type, and then the text of the exception,
 
1044
            # seems to be the best we can do.
 
1045
            import sys
 
1046
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
1047
            sys.stderr.write('%s\n\n' % (e,))
1047
1048
        server.finish_subsystem()
1048
1049
 
1049
1050
 
1052
1053
 
1053
1054
    def get_url(self):
1054
1055
        """See bzrlib.transport.Server.get_url."""
1055
 
        if sys.platform == 'win32':
1056
 
            return self._get_sftp_url(urlutils.escape(self._homedir))
1057
 
        else:
1058
 
            return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
 
1056
        homedir = self._homedir
 
1057
        if sys.platform != 'win32':
 
1058
            # Remove the initial '/' on all platforms but win32
 
1059
            homedir = homedir[1:]
 
1060
        return self._get_sftp_url(urlutils.escape(homedir))
1059
1061
 
1060
1062
 
1061
1063
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1067
1069
 
1068
1070
 
1069
1071
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1070
 
    """A test servere for sftp transports, using absolute urls to non-home."""
1071
 
 
1072
 
    def setUp(self):
 
1072
    """A test server for sftp transports where only absolute paths will work.
 
1073
 
 
1074
    It does this by serving from a deeply-nested directory that doesn't exist.
 
1075
    """
 
1076
 
 
1077
    def setUp(self, backing_server=None):
1073
1078
        self._server_homedir = '/dev/noone/runs/tests/here'
1074
 
        super(SFTPSiblingAbsoluteServer, self).setUp()
1075
 
 
1076
 
 
1077
 
def _sftp_connect(host, port, username, password):
1078
 
    """Connect to the remote sftp server.
1079
 
 
1080
 
    :raises: a TransportError 'could not connect'.
1081
 
 
1082
 
    :returns: an paramiko.sftp_client.SFTPClient
1083
 
 
1084
 
    TODO: Raise a more reasonable ConnectionFailed exception
1085
 
    """
1086
 
    idx = (host, port, username)
1087
 
    try:
1088
 
        return _connected_hosts[idx]
1089
 
    except KeyError:
1090
 
        pass
1091
 
    
1092
 
    sftp = _sftp_connect_uncached(host, port, username, password)
1093
 
    _connected_hosts[idx] = sftp
1094
 
    return sftp
1095
 
 
1096
 
def _sftp_connect_uncached(host, port, username, password):
1097
 
    vendor = ssh._get_ssh_vendor()
1098
 
    sftp = vendor.connect_sftp(username, password, host, port)
1099
 
    return sftp
 
1079
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
1100
1080
 
1101
1081
 
1102
1082
def get_test_permutations():