~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Aaron Bentley
  • Date: 2006-04-07 22:46:52 UTC
  • mfrom: (1645 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1727.
  • Revision ID: aaron.bentley@utoronto.ca-20060407224652-4925bc3735b926f8
Merged latest bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import errno
20
20
import getpass
21
21
import os
 
22
import random
22
23
import re
23
24
import stat
 
25
import subprocess
24
26
import sys
 
27
import time
25
28
import urllib
26
29
import urlparse
27
 
import time
28
 
import random
29
 
import subprocess
30
30
import weakref
31
31
 
32
 
from bzrlib.errors import (FileExists, 
 
32
from bzrlib.config import config_dir, ensure_config_dir_exists
 
33
from bzrlib.errors import (ConnectionError,
 
34
                           FileExists, 
33
35
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
36
                           TransportError,
35
 
                           LockError)
36
 
from bzrlib.config import config_dir
 
37
                           LockError, ParamikoNotPresent
 
38
                           )
 
39
from bzrlib.osutils import pathjoin, fancy_rename
37
40
from bzrlib.trace import mutter, warning, error
38
 
from bzrlib.transport import Transport, register_transport
 
41
from bzrlib.transport import (
 
42
    register_urlparse_netloc_protocol,
 
43
    Server,
 
44
    Transport,
 
45
    urlescape,
 
46
    )
39
47
import bzrlib.ui
40
48
 
41
49
try:
42
50
    import paramiko
43
 
except ImportError:
44
 
    error('The SFTP transport requires paramiko.')
45
 
    raise
 
51
except ImportError, e:
 
52
    raise ParamikoNotPresent(e)
46
53
else:
47
54
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
48
55
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
51
58
    from paramiko.sftp_file import SFTPFile
52
59
    from paramiko.sftp_client import SFTPClient
53
60
 
54
 
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
61
 
 
62
register_urlparse_netloc_protocol('sftp')
 
63
 
 
64
 
 
65
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
 
66
_default_do_prefetch = False
 
67
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 2):
 
68
    _default_do_prefetch = True
55
69
 
56
70
 
57
71
_close_fds = True
60
74
    _close_fds = False
61
75
 
62
76
_ssh_vendor = None
 
77
 
63
78
def _get_ssh_vendor():
64
79
    """Find out what version of SSH is on the system."""
65
80
    global _ssh_vendor
68
83
 
69
84
    _ssh_vendor = 'none'
70
85
 
 
86
    if 'BZR_SSH' in os.environ:
 
87
        _ssh_vendor = os.environ['BZR_SSH']
 
88
        if _ssh_vendor == 'paramiko':
 
89
            _ssh_vendor = 'none'
 
90
        return _ssh_vendor
 
91
 
71
92
    try:
72
93
        p = subprocess.Popen(['ssh', '-V'],
73
94
                             close_fds=_close_fds,
98
119
 
99
120
class SFTPSubprocess:
100
121
    """A socket-like object that talks to an ssh subprocess via pipes."""
101
 
    def __init__(self, hostname, port=None, user=None):
102
 
        vendor = _get_ssh_vendor()
 
122
    def __init__(self, hostname, vendor, port=None, user=None):
103
123
        assert vendor in ['openssh', 'ssh']
104
124
        if vendor == 'openssh':
105
125
            args = ['ssh',
126
146
    def send(self, data):
127
147
        return os.write(self.proc.stdin.fileno(), data)
128
148
 
 
149
    def recv_ready(self):
 
150
        # TODO: jam 20051215 this function is necessary to support the
 
151
        # pipelined() function. In reality, it probably should use
 
152
        # poll() or select() to actually return if there is data
 
153
        # available, otherwise we probably don't get any benefit
 
154
        return True
 
155
 
129
156
    def recv(self, count):
130
157
        return os.read(self.proc.stdout.fileno(), count)
131
158
 
135
162
        self.proc.wait()
136
163
 
137
164
 
 
165
class LoopbackSFTP(object):
 
166
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
167
 
 
168
    def __init__(self, sock):
 
169
        self.__socket = sock
 
170
 
 
171
    def send(self, data):
 
172
        return self.__socket.send(data)
 
173
 
 
174
    def recv(self, n):
 
175
        return self.__socket.recv(n)
 
176
 
 
177
    def recv_ready(self):
 
178
        return True
 
179
 
 
180
    def close(self):
 
181
        self.__socket.close()
 
182
 
 
183
 
138
184
SYSTEM_HOSTKEYS = {}
139
185
BZR_HOSTKEYS = {}
140
186
 
144
190
# X seconds. But that requires a lot more fanciness.
145
191
_connected_hosts = weakref.WeakValueDictionary()
146
192
 
 
193
 
147
194
def load_host_keys():
148
195
    """
149
196
    Load system host keys (probably doesn't work on windows) and any
154
201
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
155
202
    except Exception, e:
156
203
        mutter('failed to load system host keys: ' + str(e))
157
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
 
204
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
158
205
    try:
159
206
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
160
207
    except Exception, e:
161
208
        mutter('failed to load bzr host keys: ' + str(e))
162
209
        save_host_keys()
163
210
 
 
211
 
164
212
def save_host_keys():
165
213
    """
166
214
    Save "discovered" host keys in $(config)/ssh_host_keys/.
167
215
    """
168
216
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
169
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
170
 
    if not os.path.isdir(config_dir()):
171
 
        os.mkdir(config_dir())
 
217
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
218
    ensure_config_dir_exists()
 
219
 
172
220
    try:
173
221
        f = open(bzr_hostkey_path, 'w')
174
222
        f.write('# SSH host keys collected by bzr\n')
191
239
        self.lock_path = path + '.write-lock'
192
240
        self.transport = transport
193
241
        try:
194
 
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
 
242
            # RBC 20060103 FIXME should we be using private methods here ?
 
243
            abspath = transport._remote_path(self.lock_path)
 
244
            self.lock_file = transport._sftp_open_exclusive(abspath)
195
245
        except FileExists:
196
246
            raise LockError('File %r already locked' % (self.path,))
197
247
 
198
248
    def __del__(self):
199
249
        """Should this warn, or actually try to cleanup?"""
200
250
        if self.lock_file:
201
 
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
 
251
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
202
252
            self.unlock()
203
253
 
204
254
    def unlock(self):
216
266
    """
217
267
    Transport implementation for SFTP access.
218
268
    """
219
 
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
 
269
    _do_prefetch = _default_do_prefetch
220
270
 
221
271
    def __init__(self, base, clone_from=None):
222
272
        assert base.startswith('sftp://')
223
273
        self._parse_url(base)
224
274
        base = self._unparse_url()
 
275
        if base[-1] != '/':
 
276
            base += '/'
225
277
        super(SFTPTransport, self).__init__(base)
226
278
        if clone_from is None:
227
279
            self._sftp_connect()
254
306
        @param relpath: the relative path or path components
255
307
        @type relpath: str or list
256
308
        """
257
 
        return self._unparse_url(self._abspath(relpath))
 
309
        return self._unparse_url(self._remote_path(relpath))
258
310
    
259
 
    def _abspath(self, relpath):
260
 
        """Return the absolute path segment without the SFTP URL."""
 
311
    def _remote_path(self, relpath):
 
312
        """Return the path to be passed along the sftp protocol for relpath.
 
313
        
 
314
        relpath is a urlencoded string.
 
315
        """
261
316
        # FIXME: share the common code across transports
262
317
        assert isinstance(relpath, basestring)
263
 
        relpath = [urllib.unquote(relpath)]
 
318
        relpath = urllib.unquote(relpath).split('/')
264
319
        basepath = self._path.split('/')
265
320
        if len(basepath) > 0 and basepath[-1] == '':
266
321
            basepath = basepath[:-1]
278
333
                basepath.append(p)
279
334
 
280
335
        path = '/'.join(basepath)
281
 
        # could still be a "relative" path here, but relative on the sftp server
282
336
        return path
283
337
 
284
338
    def relpath(self, abspath):
296
350
            extra = ': ' + ', '.join(error)
297
351
            raise PathNotChild(abspath, self.base, extra=extra)
298
352
        pl = len(self._path)
299
 
        return path[pl:].lstrip('/')
 
353
        return path[pl:].strip('/')
300
354
 
301
355
    def has(self, relpath):
302
356
        """
303
357
        Does the target location exist?
304
358
        """
305
359
        try:
306
 
            self._sftp.stat(self._abspath(relpath))
 
360
            self._sftp.stat(self._remote_path(relpath))
307
361
            return True
308
362
        except IOError:
309
363
            return False
310
364
 
311
 
    def get(self, relpath, decode=False):
 
365
    def get(self, relpath):
312
366
        """
313
367
        Get the file at the given relative path.
314
368
 
315
369
        :param relpath: The relative path to the file
316
370
        """
317
371
        try:
318
 
            path = self._abspath(relpath)
319
 
            f = self._sftp.file(path)
320
 
            if self._do_prefetch and hasattr(f, 'prefetch'):
 
372
            path = self._remote_path(relpath)
 
373
            f = self._sftp.file(path, mode='rb')
 
374
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
321
375
                f.prefetch()
322
376
            return f
323
377
        except (IOError, paramiko.SSHException), e:
342
396
            f.prefetch()
343
397
        return f
344
398
 
345
 
    def put(self, relpath, f):
 
399
    def put(self, relpath, f, mode=None):
346
400
        """
347
401
        Copy the file-like or string object into the location.
348
402
 
349
403
        :param relpath: Location to put the contents, relative to base.
350
404
        :param f:       File-like or string object.
 
405
        :param mode: The final mode for the file
351
406
        """
352
 
        final_path = self._abspath(relpath)
353
 
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
 
407
        final_path = self._remote_path(relpath)
 
408
        self._put(final_path, f, mode=mode)
 
409
 
 
410
    def _put(self, abspath, f, mode=None):
 
411
        """Helper function so both put() and copy_abspaths can reuse the code"""
 
412
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
354
413
                        os.getpid(), random.randint(0,0x7FFFFFFF))
355
 
        tmp_abspath = self._abspath(tmp_relpath)
356
 
        fout = self._sftp_open_exclusive(tmp_relpath)
357
 
 
 
414
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
 
415
        closed = False
358
416
        try:
359
417
            try:
 
418
                fout.set_pipelined(True)
360
419
                self._pump(f, fout)
361
 
            except (paramiko.SSHException, IOError), e:
362
 
                self._translate_io_exception(e, relpath, ': unable to write')
 
420
            except (IOError, paramiko.SSHException), e:
 
421
                self._translate_io_exception(e, tmp_abspath)
 
422
            if mode is not None:
 
423
                self._sftp.chmod(tmp_abspath, mode)
 
424
            fout.close()
 
425
            closed = True
 
426
            self._rename_and_overwrite(tmp_abspath, abspath)
363
427
        except Exception, e:
364
428
            # If we fail, try to clean up the temporary file
365
429
            # before we throw the exception
366
430
            # but don't let another exception mess things up
 
431
            # Write out the traceback, because otherwise
 
432
            # the catch and throw destroys it
 
433
            import traceback
 
434
            mutter(traceback.format_exc())
367
435
            try:
368
 
                fout.close()
 
436
                if not closed:
 
437
                    fout.close()
369
438
                self._sftp.remove(tmp_abspath)
370
439
            except:
371
 
                pass
372
 
            raise e
373
 
        else:
374
 
            # sftp rename doesn't allow overwriting, so play tricks:
375
 
            tmp_safety = 'bzr.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0, 0x7FFFFFFF))
376
 
            tmp_safety = self._abspath(tmp_safety)
377
 
            try:
378
 
                self._sftp.rename(final_path, tmp_safety)
379
 
                file_existed = True
380
 
            except:
381
 
                file_existed = False
382
 
            success = False
383
 
            try:
384
 
                try:
385
 
                    self._sftp.rename(tmp_abspath, final_path)
386
 
                except (paramiko.SSHException, IOError), e:
387
 
                    self._translate_io_exception(e, relpath, ': unable to rename')
388
 
                else:
389
 
                    success = True
390
 
            finally:
391
 
                if file_existed:
392
 
                    if success:
393
 
                        self._sftp.unlink(tmp_safety)
394
 
                    else:
395
 
                        self._sftp.rename(tmp_safety, final_path)
 
440
                # raise the saved except
 
441
                raise e
 
442
            # raise the original with its traceback if we can.
 
443
            raise
396
444
 
397
445
    def iter_files_recursive(self):
398
446
        """Walk the relative paths of all files in this transport."""
406
454
            else:
407
455
                yield relpath
408
456
 
409
 
    def mkdir(self, relpath):
 
457
    def mkdir(self, relpath, mode=None):
410
458
        """Create a directory at the given path."""
411
459
        try:
412
 
            path = self._abspath(relpath)
 
460
            path = self._remote_path(relpath)
 
461
            # In the paramiko documentation, it says that passing a mode flag 
 
462
            # will filtered against the server umask.
 
463
            # StubSFTPServer does not do this, which would be nice, because it is
 
464
            # what we really want :)
 
465
            # However, real servers do use umask, so we really should do it that way
413
466
            self._sftp.mkdir(path)
 
467
            if mode is not None:
 
468
                self._sftp.chmod(path, mode=mode)
414
469
        except (paramiko.SSHException, IOError), e:
415
 
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
470
            self._translate_io_exception(e, path, ': unable to mkdir',
416
471
                failure_exc=FileExists)
417
472
 
418
473
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
439
494
            # strange but true, for the paramiko server.
440
495
            if (e.args == ('Failure',)):
441
496
                raise failure_exc(path, str(e) + more_info)
 
497
            mutter('Raising exception with args %s', e.args)
 
498
        if hasattr(e, 'errno'):
 
499
            mutter('Raising exception with errno %s', e.errno)
442
500
        raise e
443
501
 
444
502
    def append(self, relpath, f):
447
505
        location.
448
506
        """
449
507
        try:
450
 
            path = self._abspath(relpath)
 
508
            path = self._remote_path(relpath)
451
509
            fout = self._sftp.file(path, 'ab')
 
510
            result = fout.tell()
452
511
            self._pump(f, fout)
 
512
            return result
453
513
        except (IOError, paramiko.SSHException), e:
454
514
            self._translate_io_exception(e, relpath, ': unable to append')
455
515
 
456
 
    def copy(self, rel_from, rel_to):
457
 
        """Copy the item at rel_from to the location at rel_to"""
458
 
        path_from = self._abspath(rel_from)
459
 
        path_to = self._abspath(rel_to)
460
 
        self._copy_abspaths(path_from, path_to)
461
 
 
462
 
    def _copy_abspaths(self, path_from, path_to):
463
 
        """Copy files given an absolute path
464
 
 
465
 
        :param path_from: Path on remote server to read
466
 
        :param path_to: Path on remote server to write
467
 
        :return: None
468
 
 
469
 
        TODO: Should the destination location be atomically created?
470
 
              This has not been specified
471
 
        TODO: This should use some sort of remote copy, rather than
472
 
              pulling the data locally, and then writing it remotely
473
 
        """
474
 
        try:
475
 
            fin = self._sftp.file(path_from, 'rb')
476
 
            try:
477
 
                fout = self._sftp.file(path_to, 'wb')
478
 
                try:
479
 
                    fout.set_pipelined(True)
480
 
                    self._pump(fin, fout)
481
 
                finally:
482
 
                    fout.close()
483
 
            finally:
484
 
                fin.close()
485
 
        except (IOError, paramiko.SSHException), e:
486
 
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
487
 
 
488
 
    def copy_to(self, relpaths, other, pb=None):
489
 
        """Copy a set of entries from self into another Transport.
490
 
 
491
 
        :param relpaths: A list/generator of entries to be copied.
492
 
        """
493
 
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
494
 
            # Both from & to are on the same remote filesystem
495
 
            # We can use a remote copy, instead of pulling locally, and pushing
496
 
 
497
 
            total = self._get_total(relpaths)
498
 
            count = 0
499
 
            for path in relpaths:
500
 
                path_from = self._abspath(relpath)
501
 
                path_to = other._abspath(relpath)
502
 
                self._update_pb(pb, 'copy-to', count, total)
503
 
                self._copy_abspaths(path_from, path_to)
504
 
                count += 1
505
 
            return count
506
 
        else:
507
 
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
508
 
 
509
 
        # The dummy implementation just does a simple get + put
510
 
        def copy_entry(path):
511
 
            other.put(path, self.get(path))
512
 
 
513
 
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
516
    def rename(self, rel_from, rel_to):
 
517
        """Rename without special overwriting"""
 
518
        try:
 
519
            self._sftp.rename(self._remote_path(rel_from),
 
520
                              self._remote_path(rel_to))
 
521
        except (IOError, paramiko.SSHException), e:
 
522
            self._translate_io_exception(e, rel_from,
 
523
                    ': unable to rename to %r' % (rel_to))
 
524
 
 
525
    def _rename_and_overwrite(self, abs_from, abs_to):
 
526
        """Do a fancy rename on the remote server.
 
527
        
 
528
        Using the implementation provided by osutils.
 
529
        """
 
530
        try:
 
531
            fancy_rename(abs_from, abs_to,
 
532
                    rename_func=self._sftp.rename,
 
533
                    unlink_func=self._sftp.remove)
 
534
        except (IOError, paramiko.SSHException), e:
 
535
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
514
536
 
515
537
    def move(self, rel_from, rel_to):
516
538
        """Move the item at rel_from to the location at rel_to"""
517
 
        path_from = self._abspath(rel_from)
518
 
        path_to = self._abspath(rel_to)
519
 
        try:
520
 
            self._sftp.rename(path_from, path_to)
521
 
        except (IOError, paramiko.SSHException), e:
522
 
            self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
 
539
        path_from = self._remote_path(rel_from)
 
540
        path_to = self._remote_path(rel_to)
 
541
        self._rename_and_overwrite(path_from, path_to)
523
542
 
524
543
    def delete(self, relpath):
525
544
        """Delete the item at relpath"""
526
 
        path = self._abspath(relpath)
 
545
        path = self._remote_path(relpath)
527
546
        try:
528
547
            self._sftp.remove(path)
529
548
        except (IOError, paramiko.SSHException), e:
538
557
        Return a list of all files at the given location.
539
558
        """
540
559
        # does anything actually use this?
541
 
        path = self._abspath(relpath)
 
560
        path = self._remote_path(relpath)
542
561
        try:
543
562
            return self._sftp.listdir(path)
544
563
        except (IOError, paramiko.SSHException), e:
545
564
            self._translate_io_exception(e, path, ': failed to list_dir')
546
565
 
 
566
    def rmdir(self, relpath):
 
567
        """See Transport.rmdir."""
 
568
        path = self._remote_path(relpath)
 
569
        try:
 
570
            return self._sftp.rmdir(path)
 
571
        except (IOError, paramiko.SSHException), e:
 
572
            self._translate_io_exception(e, path, ': failed to rmdir')
 
573
 
547
574
    def stat(self, relpath):
548
575
        """Return the stat information for a file."""
549
 
        path = self._abspath(relpath)
 
576
        path = self._remote_path(relpath)
550
577
        try:
551
578
            return self._sftp.stat(path)
552
579
        except (IOError, paramiko.SSHException), e:
578
605
        # that we have taken the lock.
579
606
        return SFTPLock(relpath, self)
580
607
 
581
 
 
582
608
    def _unparse_url(self, path=None):
583
609
        if path is None:
584
610
            path = self._path
585
611
        path = urllib.quote(path)
586
 
        if path.startswith('/'):
587
 
            path = '/%2F' + path[1:]
588
 
        else:
589
 
            path = '/' + path
 
612
        # handle homedir paths
 
613
        if not path.startswith('/'):
 
614
            path = "/~/" + path
590
615
        netloc = urllib.quote(self._host)
591
616
        if self._username is not None:
592
617
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
593
618
        if self._port is not None:
594
619
            netloc = '%s:%d' % (netloc, self._port)
595
 
 
596
620
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
597
621
 
598
622
    def _split_url(self, url):
626
650
        # as a homedir relative path (the path begins with a double slash
627
651
        # if it is absolute).
628
652
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
629
 
        if path.startswith('/'):
630
 
            path = path[1:]
631
 
 
 
653
        # RBC 20060118 we are not using this as its too user hostile. instead
 
654
        # we are following lftp and using /~/foo to mean '~/foo'.
 
655
        # handle homedir paths
 
656
        if path.startswith('/~/'):
 
657
            path = path[3:]
 
658
        elif path == '/~':
 
659
            path = ''
632
660
        return (username, password, host, port, path)
633
661
 
634
662
    def _parse_url(self, url):
652
680
            pass
653
681
        
654
682
        vendor = _get_ssh_vendor()
655
 
        if vendor != 'none':
656
 
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
683
        if vendor == 'loopback':
 
684
            sock = socket.socket()
 
685
            sock.connect((self._host, self._port))
 
686
            self._sftp = SFTPClient(LoopbackSFTP(sock))
 
687
        elif vendor != 'none':
 
688
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
689
                                  self._username)
657
690
            self._sftp = SFTPClient(sock)
658
691
        else:
659
692
            self._paramiko_connect()
667
700
 
668
701
        try:
669
702
            t = paramiko.Transport((self._host, self._port or 22))
 
703
            t.set_log_channel('bzr.paramiko')
670
704
            t.start_client()
671
705
        except paramiko.SSHException, e:
672
706
            raise ConnectionError('Unable to reach SSH host %s:%d' %
691
725
            save_host_keys()
692
726
        if server_key != our_server_key:
693
727
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
694
 
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
 
728
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
695
729
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
696
730
                (self._host, our_server_key_hex, server_key_hex),
697
731
                ['Try editing %s or %s' % (filename1, filename2)])
733
767
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
734
768
            return
735
769
 
736
 
 
737
770
        if self._password:
738
771
            try:
739
772
                transport.auth_password(username, self._password)
776
809
            pass
777
810
        return False
778
811
 
779
 
    def _sftp_open_exclusive(self, relpath):
 
812
    def _sftp_open_exclusive(self, abspath, mode=None):
780
813
        """Open a remote path exclusively.
781
814
 
782
815
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
787
820
        WARNING: This breaks the SFTPClient abstraction, so it
788
821
        could easily break against an updated version of paramiko.
789
822
 
790
 
        :param relpath: The relative path, where the file should be opened
 
823
        :param abspath: The remote absolute path where the file should be opened
 
824
        :param mode: The mode permissions bits for the new file
791
825
        """
792
 
        path = self._sftp._adjust_cwd(self._abspath(relpath))
 
826
        path = self._sftp._adjust_cwd(abspath)
793
827
        attr = SFTPAttributes()
794
 
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
 
828
        if mode is not None:
 
829
            attr.st_mode = mode
 
830
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
795
831
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
796
832
        try:
797
 
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
 
833
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
798
834
            if t != CMD_HANDLE:
799
835
                raise TransportError('Expected an SFTP handle')
800
836
            handle = msg.get_string()
801
 
            return SFTPFile(self._sftp, handle, 'w', -1)
 
837
            return SFTPFile(self._sftp, handle, 'wb', -1)
802
838
        except (paramiko.SSHException, IOError), e:
803
 
            self._translate_io_exception(e, relpath, ': unable to open',
 
839
            self._translate_io_exception(e, abspath, ': unable to open',
804
840
                failure_exc=FileExists)
805
841
 
 
842
 
 
843
# ------------- server test implementation --------------
 
844
import socket
 
845
import threading
 
846
 
 
847
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
 
848
 
 
849
STUB_SERVER_KEY = """
 
850
-----BEGIN RSA PRIVATE KEY-----
 
851
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
852
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
853
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
854
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
855
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
856
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
857
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
858
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
859
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
860
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
861
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
862
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
863
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
864
-----END RSA PRIVATE KEY-----
 
865
"""
 
866
    
 
867
 
 
868
class SingleListener(threading.Thread):
 
869
 
 
870
    def __init__(self, callback):
 
871
        threading.Thread.__init__(self)
 
872
        self._callback = callback
 
873
        self._socket = socket.socket()
 
874
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
875
        self._socket.bind(('localhost', 0))
 
876
        self._socket.listen(1)
 
877
        self.port = self._socket.getsockname()[1]
 
878
        self.stop_event = threading.Event()
 
879
 
 
880
    def run(self):
 
881
        s, _ = self._socket.accept()
 
882
        # now close the listen socket
 
883
        self._socket.close()
 
884
        try:
 
885
            self._callback(s, self.stop_event)
 
886
        except socket.error:
 
887
            pass #Ignore socket errors
 
888
        except Exception, x:
 
889
            # probably a failed test
 
890
            warning('Exception from within unit test server thread: %r' % x)
 
891
 
 
892
    def stop(self):
 
893
        self.stop_event.set()
 
894
        # use a timeout here, because if the test fails, the server thread may
 
895
        # never notice the stop_event.
 
896
        self.join(5.0)
 
897
 
 
898
 
 
899
class SFTPServer(Server):
 
900
    """Common code for SFTP server facilities."""
 
901
 
 
902
    def __init__(self):
 
903
        self._original_vendor = None
 
904
        self._homedir = None
 
905
        self._server_homedir = None
 
906
        self._listener = None
 
907
        self._root = None
 
908
        self._vendor = 'none'
 
909
        # sftp server logs
 
910
        self.logs = []
 
911
 
 
912
    def _get_sftp_url(self, path):
 
913
        """Calculate an sftp url to this server for path."""
 
914
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
 
915
 
 
916
    def log(self, message):
 
917
        """StubServer uses this to log when a new server is created."""
 
918
        self.logs.append(message)
 
919
 
 
920
    def _run_server(self, s, stop_event):
 
921
        ssh_server = paramiko.Transport(s)
 
922
        key_file = os.path.join(self._homedir, 'test_rsa.key')
 
923
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
924
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
925
        ssh_server.add_server_key(host_key)
 
926
        server = StubServer(self)
 
927
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
928
                                         StubSFTPServer, root=self._root,
 
929
                                         home=self._server_homedir)
 
930
        event = threading.Event()
 
931
        ssh_server.start_server(event, server)
 
932
        event.wait(5.0)
 
933
        stop_event.wait(30.0)
 
934
    
 
935
    def setUp(self):
 
936
        global _ssh_vendor
 
937
        self._original_vendor = _ssh_vendor
 
938
        _ssh_vendor = self._vendor
 
939
        self._homedir = os.getcwdu()
 
940
        if self._server_homedir is None:
 
941
            self._server_homedir = self._homedir
 
942
        self._root = '/'
 
943
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
 
944
        self._listener = SingleListener(self._run_server)
 
945
        self._listener.setDaemon(True)
 
946
        self._listener.start()
 
947
 
 
948
    def tearDown(self):
 
949
        """See bzrlib.transport.Server.tearDown."""
 
950
        global _ssh_vendor
 
951
        self._listener.stop()
 
952
        _ssh_vendor = self._original_vendor
 
953
 
 
954
 
 
955
class SFTPFullAbsoluteServer(SFTPServer):
 
956
    """A test server for sftp transports, using absolute urls and ssh."""
 
957
 
 
958
    def get_url(self):
 
959
        """See bzrlib.transport.Server.get_url."""
 
960
        return self._get_sftp_url(urlescape(self._homedir[1:]))
 
961
 
 
962
 
 
963
class SFTPServerWithoutSSH(SFTPServer):
 
964
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
 
965
 
 
966
    def __init__(self):
 
967
        super(SFTPServerWithoutSSH, self).__init__()
 
968
        self._vendor = 'loopback'
 
969
 
 
970
    def _run_server(self, sock, stop_event):
 
971
        class FakeChannel(object):
 
972
            def get_transport(self):
 
973
                return self
 
974
            def get_log_channel(self):
 
975
                return 'paramiko'
 
976
            def get_name(self):
 
977
                return '1'
 
978
            def get_hexdump(self):
 
979
                return False
 
980
 
 
981
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
982
                                     root=self._root, home=self._server_homedir)
 
983
        server.start_subsystem('sftp', None, sock)
 
984
        server.finish_subsystem()
 
985
 
 
986
 
 
987
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
 
988
    """A test server for sftp transports, using absolute urls."""
 
989
 
 
990
    def get_url(self):
 
991
        """See bzrlib.transport.Server.get_url."""
 
992
        return self._get_sftp_url(urlescape(self._homedir[1:]))
 
993
 
 
994
 
 
995
class SFTPHomeDirServer(SFTPServerWithoutSSH):
 
996
    """A test server for sftp transports, using homedir relative urls."""
 
997
 
 
998
    def get_url(self):
 
999
        """See bzrlib.transport.Server.get_url."""
 
1000
        return self._get_sftp_url("~/")
 
1001
 
 
1002
 
 
1003
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
1004
    """A test servere for sftp transports, using absolute urls to non-home."""
 
1005
 
 
1006
    def setUp(self):
 
1007
        self._server_homedir = '/dev/noone/runs/tests/here'
 
1008
        super(SFTPSiblingAbsoluteServer, self).setUp()
 
1009
 
 
1010
 
 
1011
def get_test_permutations():
 
1012
    """Return the permutations to be used in testing."""
 
1013
    return [(SFTPTransport, SFTPAbsoluteServer),
 
1014
            (SFTPTransport, SFTPHomeDirServer),
 
1015
            (SFTPTransport, SFTPSiblingAbsoluteServer),
 
1016
            ]