~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

merge integration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
import subprocess
30
30
import weakref
31
31
 
 
32
from bzrlib.config import config_dir, ensure_config_dir_exists
32
33
from bzrlib.errors import (ConnectionError,
33
34
                           FileExists, 
34
35
                           TransportNotPossible, NoSuchFile, PathNotChild,
35
36
                           TransportError,
36
37
                           LockError
37
38
                           )
38
 
from bzrlib.config import config_dir
 
39
from bzrlib.osutils import pathjoin, fancy_rename
39
40
from bzrlib.trace import mutter, warning, error
40
41
from bzrlib.transport import Transport, Server, urlescape
41
42
import bzrlib.ui
127
128
    def send(self, data):
128
129
        return os.write(self.proc.stdin.fileno(), data)
129
130
 
 
131
    def recv_ready(self):
 
132
        # TODO: jam 20051215 this function is necessary to support the
 
133
        # pipelined() function. In reality, it probably should use
 
134
        # poll() or select() to actually return if there is data
 
135
        # available, otherwise we probably don't get any benefit
 
136
        return True
 
137
 
130
138
    def recv(self, count):
131
139
        return os.read(self.proc.stdout.fileno(), count)
132
140
 
155
163
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
156
164
    except Exception, e:
157
165
        mutter('failed to load system host keys: ' + str(e))
158
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
 
166
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
159
167
    try:
160
168
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
161
169
    except Exception, e:
167
175
    Save "discovered" host keys in $(config)/ssh_host_keys/.
168
176
    """
169
177
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
170
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
171
 
    if not os.path.isdir(config_dir()):
172
 
        os.mkdir(config_dir())
 
178
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
179
    ensure_config_dir_exists()
 
180
 
173
181
    try:
174
182
        f = open(bzr_hostkey_path, 'w')
175
183
        f.write('# SSH host keys collected by bzr\n')
192
200
        self.lock_path = path + '.write-lock'
193
201
        self.transport = transport
194
202
        try:
195
 
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
 
203
            # RBC 20060103 FIXME should we be using private methods here ?
 
204
            abspath = transport._remote_path(self.lock_path)
 
205
            self.lock_file = transport._sftp_open_exclusive(abspath)
196
206
        except FileExists:
197
207
            raise LockError('File %r already locked' % (self.path,))
198
208
 
323
333
        """
324
334
        try:
325
335
            path = self._remote_path(relpath)
326
 
            f = self._sftp.file(path)
 
336
            f = self._sftp.file(path, mode='rb')
327
337
            if self._do_prefetch and hasattr(f, 'prefetch'):
328
338
                f.prefetch()
329
339
            return f
349
359
            f.prefetch()
350
360
        return f
351
361
 
352
 
    def put(self, relpath, f):
 
362
    def put(self, relpath, f, mode=None):
353
363
        """
354
364
        Copy the file-like or string object into the location.
355
365
 
356
366
        :param relpath: Location to put the contents, relative to base.
357
367
        :param f:       File-like or string object.
 
368
        :param mode: The final mode for the file
358
369
        """
359
370
        final_path = self._remote_path(relpath)
360
 
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
 
371
        self._put(final_path, f, mode=mode)
 
372
 
 
373
    def _put(self, abspath, f, mode=None):
 
374
        """Helper function so both put() and copy_abspaths can reuse the code"""
 
375
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
361
376
                        os.getpid(), random.randint(0,0x7FFFFFFF))
362
 
        tmp_abspath = self._remote_path(tmp_relpath)
363
 
        fout = self._sftp_open_exclusive(tmp_relpath)
364
 
 
 
377
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
 
378
        closed = False
365
379
        try:
366
380
            try:
 
381
                fout.set_pipelined(True)
367
382
                self._pump(f, fout)
368
 
            except (paramiko.SSHException, IOError), e:
369
 
                self._translate_io_exception(e, relpath, ': unable to write')
 
383
            except (IOError, paramiko.SSHException), e:
 
384
                self._translate_io_exception(e, tmp_abspath)
 
385
            if mode is not None:
 
386
                self._sftp.chmod(tmp_abspath, mode)
 
387
            fout.close()
 
388
            closed = True
 
389
            self._rename(tmp_abspath, abspath)
370
390
        except Exception, e:
371
391
            # If we fail, try to clean up the temporary file
372
392
            # before we throw the exception
373
393
            # but don't let another exception mess things up
 
394
            # Write out the traceback, because otherwise
 
395
            # the catch and throw destroys it
 
396
            import traceback
 
397
            mutter(traceback.format_exc())
374
398
            try:
375
 
                fout.close()
 
399
                if not closed:
 
400
                    fout.close()
376
401
                self._sftp.remove(tmp_abspath)
377
402
            except:
378
 
                pass
379
 
            raise e
380
 
        else:
381
 
            # sftp rename doesn't allow overwriting, so play tricks:
382
 
            tmp_safety = 'bzr.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0, 0x7FFFFFFF))
383
 
            tmp_safety = self._remote_path(tmp_safety)
384
 
            try:
385
 
                self._sftp.rename(final_path, tmp_safety)
386
 
                file_existed = True
387
 
            except:
388
 
                file_existed = False
389
 
            success = False
390
 
            try:
391
 
                try:
392
 
                    self._sftp.rename(tmp_abspath, final_path)
393
 
                except (paramiko.SSHException, IOError), e:
394
 
                    self._translate_io_exception(e, relpath, ': unable to rename')
395
 
                else:
396
 
                    success = True
397
 
            finally:
398
 
                if file_existed:
399
 
                    if success:
400
 
                        self._sftp.unlink(tmp_safety)
401
 
                    else:
402
 
                        self._sftp.rename(tmp_safety, final_path)
 
403
                # raise the saved except
 
404
                raise e
 
405
            # raise the original with its traceback if we can.
 
406
            raise
403
407
 
404
408
    def iter_files_recursive(self):
405
409
        """Walk the relative paths of all files in this transport."""
413
417
            else:
414
418
                yield relpath
415
419
 
416
 
    def mkdir(self, relpath):
 
420
    def mkdir(self, relpath, mode=None):
417
421
        """Create a directory at the given path."""
418
422
        try:
419
423
            path = self._remote_path(relpath)
 
424
            # In the paramiko documentation, it says that passing a mode flag 
 
425
            # will filtered against the server umask.
 
426
            # StubSFTPServer does not do this, which would be nice, because it is
 
427
            # what we really want :)
 
428
            # However, real servers do use umask, so we really should do it that way
420
429
            self._sftp.mkdir(path)
 
430
            if mode is not None:
 
431
                self._sftp.chmod(path, mode=mode)
421
432
        except (paramiko.SSHException, IOError), e:
422
 
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
433
            self._translate_io_exception(e, path, ': unable to mkdir',
423
434
                failure_exc=FileExists)
424
435
 
425
436
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
446
457
            # strange but true, for the paramiko server.
447
458
            if (e.args == ('Failure',)):
448
459
                raise failure_exc(path, str(e) + more_info)
 
460
            mutter('Raising exception with args %s', e.args)
 
461
        if hasattr(e, 'errno'):
 
462
            mutter('Raising exception with errno %s', e.errno)
449
463
        raise e
450
464
 
451
465
    def append(self, relpath, f):
466
480
        path_to = self._remote_path(rel_to)
467
481
        self._copy_abspaths(path_from, path_to)
468
482
 
469
 
    def _copy_abspaths(self, path_from, path_to):
 
483
    def _copy_abspaths(self, path_from, path_to, mode=None):
470
484
        """Copy files given an absolute path
471
485
 
472
486
        :param path_from: Path on remote server to read
481
495
        try:
482
496
            fin = self._sftp.file(path_from, 'rb')
483
497
            try:
484
 
                fout = self._sftp.file(path_to, 'wb')
485
 
                try:
486
 
                    fout.set_pipelined(True)
487
 
                    self._pump(fin, fout)
488
 
                finally:
489
 
                    fout.close()
 
498
                self._put(path_to, fin, mode=mode)
490
499
            finally:
491
500
                fin.close()
492
501
        except (IOError, paramiko.SSHException), e:
493
502
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
494
503
 
495
 
    def copy_to(self, relpaths, other, pb=None):
 
504
    def copy_to(self, relpaths, other, mode=None, pb=None):
496
505
        """Copy a set of entries from self into another Transport.
497
506
 
498
507
        :param relpaths: A list/generator of entries to be copied.
507
516
                path_from = self._remote_path(relpath)
508
517
                path_to = other._remote_path(relpath)
509
518
                self._update_pb(pb, 'copy-to', count, total)
510
 
                self._copy_abspaths(path_from, path_to)
 
519
                self._copy_abspaths(path_from, path_to, mode=mode)
511
520
                count += 1
512
521
            return count
513
522
        else:
514
 
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
515
 
 
516
 
        # The dummy implementation just does a simple get + put
517
 
        def copy_entry(path):
518
 
            other.put(path, self.get(path))
519
 
 
520
 
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
 
523
            return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
524
 
 
525
    def _rename(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))
521
536
 
522
537
    def move(self, rel_from, rel_to):
523
538
        """Move the item at rel_from to the location at rel_to"""
524
539
        path_from = self._remote_path(rel_from)
525
540
        path_to = self._remote_path(rel_to)
526
 
        try:
527
 
            self._sftp.rename(path_from, path_to)
528
 
        except (IOError, paramiko.SSHException), e:
529
 
            self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
 
541
        self._rename(path_from, path_to)
530
542
 
531
543
    def delete(self, relpath):
532
544
        """Delete the item at relpath"""
699
711
            save_host_keys()
700
712
        if server_key != our_server_key:
701
713
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
702
 
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
 
714
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
703
715
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
704
716
                (self._host, our_server_key_hex, server_key_hex),
705
717
                ['Try editing %s or %s' % (filename1, filename2)])
783
795
            pass
784
796
        return False
785
797
 
786
 
    def _sftp_open_exclusive(self, relpath):
 
798
    def _sftp_open_exclusive(self, abspath, mode=None):
787
799
        """Open a remote path exclusively.
788
800
 
789
801
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
794
806
        WARNING: This breaks the SFTPClient abstraction, so it
795
807
        could easily break against an updated version of paramiko.
796
808
 
797
 
        :param relpath: The relative path, where the file should be opened
 
809
        :param abspath: The remote absolute path where the file should be opened
 
810
        :param mode: The mode permissions bits for the new file
798
811
        """
799
 
        path = self._sftp._adjust_cwd(self._remote_path(relpath))
 
812
        path = self._sftp._adjust_cwd(abspath)
800
813
        attr = SFTPAttributes()
801
 
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
 
814
        if mode is not None:
 
815
            attr.st_mode = mode
 
816
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
802
817
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
803
818
        try:
804
 
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
 
819
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
805
820
            if t != CMD_HANDLE:
806
821
                raise TransportError('Expected an SFTP handle')
807
822
            handle = msg.get_string()
808
 
            return SFTPFile(self._sftp, handle, 'w', -1)
 
823
            return SFTPFile(self._sftp, handle, 'wb', -1)
809
824
        except (paramiko.SSHException, IOError), e:
810
 
            self._translate_io_exception(e, relpath, ': unable to open',
 
825
            self._translate_io_exception(e, abspath, ': unable to open',
811
826
                failure_exc=FileExists)
812
827
 
813
828