~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] jam-integration 1495

Show diffs side-by-side

added added

removed removed

Lines of Context:
33
33
                           TransportNotPossible, NoSuchFile, PathNotChild,
34
34
                           TransportError,
35
35
                           LockError)
36
 
from bzrlib.config import config_dir
 
36
from bzrlib.config import config_dir, ensure_config_dir_exists
37
37
from bzrlib.trace import mutter, warning, error
38
38
from bzrlib.transport import Transport, register_transport
 
39
from bzrlib.osutils import pathjoin, fancy_rename
39
40
import bzrlib.ui
40
41
 
41
42
try:
126
127
    def send(self, data):
127
128
        return os.write(self.proc.stdin.fileno(), data)
128
129
 
 
130
    def recv_ready(self):
 
131
        # TODO: jam 20051215 this function is necessary to support the
 
132
        # pipelined() function. In reality, it probably should use
 
133
        # poll() or select() to actually return if there is data
 
134
        # available, otherwise we probably don't get any benefit
 
135
        return True
 
136
 
129
137
    def recv(self, count):
130
138
        return os.read(self.proc.stdout.fileno(), count)
131
139
 
154
162
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
155
163
    except Exception, e:
156
164
        mutter('failed to load system host keys: ' + str(e))
157
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
 
165
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
158
166
    try:
159
167
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
160
168
    except Exception, e:
166
174
    Save "discovered" host keys in $(config)/ssh_host_keys/.
167
175
    """
168
176
    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())
 
177
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
178
    ensure_config_dir_exists()
 
179
 
172
180
    try:
173
181
        f = open(bzr_hostkey_path, 'w')
174
182
        f.write('# SSH host keys collected by bzr\n')
191
199
        self.lock_path = path + '.write-lock'
192
200
        self.transport = transport
193
201
        try:
194
 
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
 
202
            abspath = transport._abspath(self.lock_path)
 
203
            self.lock_file = transport._sftp_open_exclusive(abspath)
195
204
        except FileExists:
196
205
            raise LockError('File %r already locked' % (self.path,))
197
206
 
316
325
        """
317
326
        try:
318
327
            path = self._abspath(relpath)
319
 
            f = self._sftp.file(path)
 
328
            f = self._sftp.file(path, mode='rb')
320
329
            if self._do_prefetch and hasattr(f, 'prefetch'):
321
330
                f.prefetch()
322
331
            return f
342
351
            f.prefetch()
343
352
        return f
344
353
 
345
 
    def put(self, relpath, f):
 
354
    def put(self, relpath, f, mode=None):
346
355
        """
347
356
        Copy the file-like or string object into the location.
348
357
 
349
358
        :param relpath: Location to put the contents, relative to base.
350
359
        :param f:       File-like or string object.
 
360
        :param mode: The final mode for the file
351
361
        """
352
362
        final_path = self._abspath(relpath)
353
 
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
 
363
        self._put(final_path, f, mode=mode)
 
364
 
 
365
    def _put(self, abspath, f, mode=None):
 
366
        """Helper function so both put() and copy_abspaths can reuse the code"""
 
367
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
354
368
                        os.getpid(), random.randint(0,0x7FFFFFFF))
355
 
        tmp_abspath = self._abspath(tmp_relpath)
356
 
        fout = self._sftp_open_exclusive(tmp_relpath)
357
 
 
 
369
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
 
370
        closed = False
358
371
        try:
359
372
            try:
 
373
                fout.set_pipelined(True)
360
374
                self._pump(f, fout)
361
 
            except (paramiko.SSHException, IOError), e:
362
 
                self._translate_io_exception(e, relpath, ': unable to write')
 
375
            except (IOError, paramiko.SSHException), e:
 
376
                self._translate_io_exception(e, tmp_abspath)
 
377
            if mode is not None:
 
378
                self._sftp.chmod(tmp_abspath, mode)
 
379
            fout.close()
 
380
            closed = True
 
381
            self._rename(tmp_abspath, abspath)
363
382
        except Exception, e:
364
383
            # If we fail, try to clean up the temporary file
365
384
            # before we throw the exception
366
385
            # but don't let another exception mess things up
 
386
            # Write out the traceback, because otherwise
 
387
            # the catch and throw destroys it
 
388
            import traceback
 
389
            mutter(traceback.format_exc())
367
390
            try:
368
 
                fout.close()
 
391
                if not closed:
 
392
                    fout.close()
369
393
                self._sftp.remove(tmp_abspath)
370
394
            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)
 
395
                # raise the saved except
 
396
                raise e
 
397
            # raise the original with its traceback if we can.
 
398
            raise
396
399
 
397
400
    def iter_files_recursive(self):
398
401
        """Walk the relative paths of all files in this transport."""
406
409
            else:
407
410
                yield relpath
408
411
 
409
 
    def mkdir(self, relpath):
 
412
    def mkdir(self, relpath, mode=None):
410
413
        """Create a directory at the given path."""
411
414
        try:
412
415
            path = self._abspath(relpath)
 
416
            # In the paramiko documentation, it says that passing a mode flag 
 
417
            # will filtered against the server umask.
 
418
            # StubSFTPServer does not do this, which would be nice, because it is
 
419
            # what we really want :)
 
420
            # However, real servers do use umask, so we really should do it that way
413
421
            self._sftp.mkdir(path)
 
422
            if mode is not None:
 
423
                self._sftp.chmod(path, mode=mode)
414
424
        except (paramiko.SSHException, IOError), e:
415
 
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
425
            self._translate_io_exception(e, path, ': unable to mkdir',
416
426
                failure_exc=FileExists)
417
427
 
418
428
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
439
449
            # strange but true, for the paramiko server.
440
450
            if (e.args == ('Failure',)):
441
451
                raise failure_exc(path, str(e) + more_info)
 
452
            mutter('Raising exception with args %s', e.args)
 
453
        if hasattr(e, 'errno'):
 
454
            mutter('Raising exception with errno %s', e.errno)
442
455
        raise e
443
456
 
444
457
    def append(self, relpath, f):
459
472
        path_to = self._abspath(rel_to)
460
473
        self._copy_abspaths(path_from, path_to)
461
474
 
462
 
    def _copy_abspaths(self, path_from, path_to):
 
475
    def _copy_abspaths(self, path_from, path_to, mode=None):
463
476
        """Copy files given an absolute path
464
477
 
465
478
        :param path_from: Path on remote server to read
474
487
        try:
475
488
            fin = self._sftp.file(path_from, 'rb')
476
489
            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()
 
490
                self._put(path_to, fin, mode=mode)
483
491
            finally:
484
492
                fin.close()
485
493
        except (IOError, paramiko.SSHException), e:
486
494
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
487
495
 
488
 
    def copy_to(self, relpaths, other, pb=None):
 
496
    def copy_to(self, relpaths, other, mode=None, pb=None):
489
497
        """Copy a set of entries from self into another Transport.
490
498
 
491
499
        :param relpaths: A list/generator of entries to be copied.
500
508
                path_from = self._abspath(relpath)
501
509
                path_to = other._abspath(relpath)
502
510
                self._update_pb(pb, 'copy-to', count, total)
503
 
                self._copy_abspaths(path_from, path_to)
 
511
                self._copy_abspaths(path_from, path_to, mode=mode)
504
512
                count += 1
505
513
            return count
506
514
        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)
 
515
            return super(SFTPTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
516
 
 
517
    def _rename(self, abs_from, abs_to):
 
518
        """Do a fancy rename on the remote server.
 
519
        
 
520
        Using the implementation provided by osutils.
 
521
        """
 
522
        try:
 
523
            fancy_rename(abs_from, abs_to,
 
524
                    rename_func=self._sftp.rename,
 
525
                    unlink_func=self._sftp.remove)
 
526
        except (IOError, paramiko.SSHException), e:
 
527
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
514
528
 
515
529
    def move(self, rel_from, rel_to):
516
530
        """Move the item at rel_from to the location at rel_to"""
517
531
        path_from = self._abspath(rel_from)
518
532
        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)
 
533
        self._rename(path_from, path_to)
523
534
 
524
535
    def delete(self, relpath):
525
536
        """Delete the item at relpath"""
691
702
            save_host_keys()
692
703
        if server_key != our_server_key:
693
704
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
694
 
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
 
705
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
695
706
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
696
707
                (self._host, our_server_key_hex, server_key_hex),
697
708
                ['Try editing %s or %s' % (filename1, filename2)])
776
787
            pass
777
788
        return False
778
789
 
779
 
    def _sftp_open_exclusive(self, relpath):
 
790
    def _sftp_open_exclusive(self, abspath, mode=None):
780
791
        """Open a remote path exclusively.
781
792
 
782
793
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
787
798
        WARNING: This breaks the SFTPClient abstraction, so it
788
799
        could easily break against an updated version of paramiko.
789
800
 
790
 
        :param relpath: The relative path, where the file should be opened
 
801
        :param abspath: The remote absolute path where the file should be opened
 
802
        :param mode: The mode permissions bits for the new file
791
803
        """
792
 
        path = self._sftp._adjust_cwd(self._abspath(relpath))
 
804
        path = self._sftp._adjust_cwd(abspath)
793
805
        attr = SFTPAttributes()
794
 
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
 
806
        if mode is not None:
 
807
            attr.st_mode = mode
 
808
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
795
809
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
796
810
        try:
797
 
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
 
811
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
798
812
            if t != CMD_HANDLE:
799
813
                raise TransportError('Expected an SFTP handle')
800
814
            handle = msg.get_string()
801
 
            return SFTPFile(self._sftp, handle, 'w', -1)
 
815
            return SFTPFile(self._sftp, handle, 'wb', -1)
802
816
        except (paramiko.SSHException, IOError), e:
803
 
            self._translate_io_exception(e, relpath, ': unable to open',
 
817
            self._translate_io_exception(e, abspath, ': unable to open',
804
818
                failure_exc=FileExists)
805
819