~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Martin Pool
  • Date: 2005-09-07 23:14:30 UTC
  • mto: (1092.2.12) (974.1.76) (1185.8.2)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: mbp@sourcefrog.net-20050907231430-097abbaee94ad75b
- docstring fix from Magnus Therning

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>, Canonical Ltd
2
 
 
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
 
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
 
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Implementation of Transport over SFTP, using paramiko."""
18
 
 
19
 
import errno
20
 
import getpass
21
 
import os
22
 
import re
23
 
import stat
24
 
import sys
25
 
import urllib
26
 
import time
27
 
import random
28
 
 
29
 
from bzrlib.errors import (FileExists, 
30
 
                           TransportNotPossible, NoSuchFile, NonRelativePath,
31
 
                           TransportError,
32
 
                           LockError)
33
 
from bzrlib.config import config_dir
34
 
from bzrlib.trace import mutter, warning, error
35
 
from bzrlib.transport import Transport, register_transport
36
 
 
37
 
try:
38
 
    import paramiko
39
 
except ImportError:
40
 
    error('The SFTP transport requires paramiko.')
41
 
    raise
42
 
else:
43
 
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
44
 
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
45
 
                               CMD_HANDLE, CMD_OPEN)
46
 
    from paramiko.sftp_attr import SFTPAttributes
47
 
    from paramiko.sftp_file import SFTPFile
48
 
 
49
 
 
50
 
SYSTEM_HOSTKEYS = {}
51
 
BZR_HOSTKEYS = {}
52
 
 
53
 
def load_host_keys():
54
 
    """
55
 
    Load system host keys (probably doesn't work on windows) and any
56
 
    "discovered" keys from previous sessions.
57
 
    """
58
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
59
 
    try:
60
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
61
 
    except Exception, e:
62
 
        mutter('failed to load system host keys: ' + str(e))
63
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
64
 
    try:
65
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
66
 
    except Exception, e:
67
 
        mutter('failed to load bzr host keys: ' + str(e))
68
 
        save_host_keys()
69
 
 
70
 
def save_host_keys():
71
 
    """
72
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
73
 
    """
74
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
75
 
    bzr_hostkey_path = os.path.join(config_dir(), 'ssh_host_keys')
76
 
    if not os.path.isdir(config_dir()):
77
 
        os.mkdir(config_dir())
78
 
    try:
79
 
        f = open(bzr_hostkey_path, 'w')
80
 
        f.write('# SSH host keys collected by bzr\n')
81
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
82
 
            for keytype, key in keys.iteritems():
83
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
84
 
        f.close()
85
 
    except IOError, e:
86
 
        mutter('failed to save bzr host keys: ' + str(e))
87
 
 
88
 
 
89
 
 
90
 
class SFTPTransportError (TransportError):
91
 
    pass
92
 
 
93
 
class SFTPLock(object):
94
 
    """This fakes a lock in a remote location."""
95
 
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
96
 
    def __init__(self, path, transport):
97
 
        assert isinstance(transport, SFTPTransport)
98
 
 
99
 
        self.lock_file = None
100
 
        self.path = path
101
 
        self.lock_path = path + '.write-lock'
102
 
        self.transport = transport
103
 
        try:
104
 
            self.lock_file = transport._sftp_open_exclusive(self.lock_path)
105
 
        except FileExists:
106
 
            raise LockError('File %r already locked' % (self.path,))
107
 
 
108
 
    def __del__(self):
109
 
        """Should this warn, or actually try to cleanup?"""
110
 
        if self.lock_file:
111
 
            warn("SFTPLock %r not explicitly unlocked" % (self.path,))
112
 
            self.unlock()
113
 
 
114
 
    def unlock(self):
115
 
        if not self.lock_file:
116
 
            return
117
 
        self.lock_file.close()
118
 
        self.lock_file = None
119
 
        try:
120
 
            self.transport.delete(self.lock_path)
121
 
        except (NoSuchFile,):
122
 
            # What specific errors should we catch here?
123
 
            pass
124
 
 
125
 
class SFTPTransport (Transport):
126
 
    """
127
 
    Transport implementation for SFTP access.
128
 
    """
129
 
 
130
 
    _url_matcher = re.compile(r'^sftp://([^:@]*(:[^@]*)?@)?(.*?)(:[^/]+)?(/.*)?$')
131
 
    
132
 
    def __init__(self, base, clone_from=None):
133
 
        assert base.startswith('sftp://')
134
 
        super(SFTPTransport, self).__init__(base)
135
 
        self._parse_url(base)
136
 
        if clone_from is None:
137
 
            self._sftp_connect()
138
 
        else:
139
 
            # use the same ssh connection, etc
140
 
            self._sftp = clone_from._sftp
141
 
        # super saves 'self.base'
142
 
    
143
 
    def should_cache(self):
144
 
        """
145
 
        Return True if the data pulled across should be cached locally.
146
 
        """
147
 
        return True
148
 
 
149
 
    def clone(self, offset=None):
150
 
        """
151
 
        Return a new SFTPTransport with root at self.base + offset.
152
 
        We share the same SFTP session between such transports, because it's
153
 
        fairly expensive to set them up.
154
 
        """
155
 
        if offset is None:
156
 
            return SFTPTransport(self.base, self)
157
 
        else:
158
 
            return SFTPTransport(self.abspath(offset), self)
159
 
 
160
 
    def abspath(self, relpath):
161
 
        """
162
 
        Return the full url to the given relative path.
163
 
        
164
 
        @param relpath: the relative path or path components
165
 
        @type relpath: str or list
166
 
        """
167
 
        return self._unparse_url(self._abspath(relpath))
168
 
    
169
 
    def _abspath(self, relpath):
170
 
        """Return the absolute path segment without the SFTP URL."""
171
 
        # FIXME: share the common code across transports
172
 
        assert isinstance(relpath, basestring)
173
 
        relpath = [urllib.unquote(relpath)]
174
 
        basepath = self._path.split('/')
175
 
        if len(basepath) > 0 and basepath[-1] == '':
176
 
            basepath = basepath[:-1]
177
 
 
178
 
        for p in relpath:
179
 
            if p == '..':
180
 
                if len(basepath) == 0:
181
 
                    # In most filesystems, a request for the parent
182
 
                    # of root, just returns root.
183
 
                    continue
184
 
                basepath.pop()
185
 
            elif p == '.':
186
 
                continue # No-op
187
 
            else:
188
 
                basepath.append(p)
189
 
 
190
 
        path = '/'.join(basepath)
191
 
        # could still be a "relative" path here, but relative on the sftp server
192
 
        return path
193
 
 
194
 
    def relpath(self, abspath):
195
 
        # FIXME: this is identical to HttpTransport -- share it
196
 
        m = self._url_matcher.match(abspath)
197
 
        path = m.group(5)
198
 
        if not path.startswith(self._path):
199
 
            raise NonRelativePath('path %r is not under base URL %r'
200
 
                           % (abspath, self.base))
201
 
        pl = len(self.base)
202
 
        return abspath[pl:].lstrip('/')
203
 
 
204
 
    def has(self, relpath):
205
 
        """
206
 
        Does the target location exist?
207
 
        """
208
 
        try:
209
 
            self._sftp.stat(self._abspath(relpath))
210
 
            return True
211
 
        except IOError:
212
 
            return False
213
 
 
214
 
    def get(self, relpath, decode=False):
215
 
        """
216
 
        Get the file at the given relative path.
217
 
 
218
 
        :param relpath: The relative path to the file
219
 
        """
220
 
        try:
221
 
            path = self._abspath(relpath)
222
 
            f = self._sftp.file(path)
223
 
            try:
224
 
                f.prefetch()
225
 
            except AttributeError:
226
 
                # only works on paramiko 1.5.1 or greater
227
 
                pass
228
 
            return f
229
 
        except (IOError, paramiko.SSHException), x:
230
 
            raise NoSuchFile('Error retrieving %s: %s' % (path, str(x)), x)
231
 
 
232
 
    def get_partial(self, relpath, start, length=None):
233
 
        """
234
 
        Get just part of a file.
235
 
 
236
 
        :param relpath: Path to the file, relative to base
237
 
        :param start: The starting position to read from
238
 
        :param length: The length to read. A length of None indicates
239
 
                       read to the end of the file.
240
 
        :return: A file-like object containing at least the specified bytes.
241
 
                 Some implementations may return objects which can be read
242
 
                 past this length, but this is not guaranteed.
243
 
        """
244
 
        # TODO: implement get_partial_multi to help with knit support
245
 
        f = self.get(relpath)
246
 
        f.seek(start)
247
 
        try:
248
 
            f.prefetch()
249
 
        except AttributeError:
250
 
            # only works on paramiko 1.5.1 or greater
251
 
            pass
252
 
        return f
253
 
 
254
 
    def put(self, relpath, f):
255
 
        """
256
 
        Copy the file-like or string object into the location.
257
 
 
258
 
        :param relpath: Location to put the contents, relative to base.
259
 
        :param f:       File-like or string object.
260
 
        """
261
 
        final_path = self._abspath(relpath)
262
 
        tmp_relpath = '%s.tmp.%.9f.%d.%d' % (relpath, time.time(),
263
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
264
 
        tmp_abspath = self._abspath(tmp_relpath)
265
 
        fout = self._sftp_open_exclusive(tmp_relpath)
266
 
 
267
 
        try:
268
 
            try:
269
 
                self._pump(f, fout)
270
 
            except IOError, e:
271
 
                self._translate_io_exception(e, relpath)
272
 
            except paramiko.SSHException, x:
273
 
                raise SFTPTransportError('Unable to write file %r' % (path,), x)
274
 
        except Exception, e:
275
 
            # If we fail, try to clean up the temporary file
276
 
            # before we throw the exception
277
 
            # but don't let another exception mess things up
278
 
            try:
279
 
                fout.close()
280
 
                self._sftp.remove(tmp_abspath)
281
 
            except:
282
 
                pass
283
 
            raise e
284
 
        else:
285
 
            # sftp rename doesn't allow overwriting, so play tricks:
286
 
            tmp_safety = 'bzr.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0, 0x7FFFFFFF))
287
 
            tmp_safety = self._abspath(tmp_safety)
288
 
            try:
289
 
                self._sftp.rename(final_path, tmp_safety)
290
 
                file_existed = True
291
 
            except:
292
 
                file_existed = False
293
 
            try:
294
 
                self._sftp.rename(tmp_abspath, final_path)
295
 
            except IOError, e:
296
 
                self._translate_io_exception(e, relpath)
297
 
            except paramiko.SSHException, x:
298
 
                raise SFTPTransportError('Unable to rename into file %r' 
299
 
                                          % (path,), x)
300
 
            if file_existed:
301
 
                self._sftp.unlink(tmp_safety)
302
 
 
303
 
    def iter_files_recursive(self):
304
 
        """Walk the relative paths of all files in this transport."""
305
 
        queue = list(self.list_dir('.'))
306
 
        while queue:
307
 
            relpath = urllib.quote(queue.pop(0))
308
 
            st = self.stat(relpath)
309
 
            if stat.S_ISDIR(st.st_mode):
310
 
                for i, basename in enumerate(self.list_dir(relpath)):
311
 
                    queue.insert(i, relpath+'/'+basename)
312
 
            else:
313
 
                yield relpath
314
 
 
315
 
    def mkdir(self, relpath):
316
 
        """Create a directory at the given path."""
317
 
        try:
318
 
            path = self._abspath(relpath)
319
 
            self._sftp.mkdir(path)
320
 
        except IOError, e:
321
 
            self._translate_io_exception(e, relpath)
322
 
        except (IOError, paramiko.SSHException), x:
323
 
            raise SFTPTransportError('Unable to mkdir %r' % (path,), x)
324
 
 
325
 
    def _translate_io_exception(self, e, relpath):
326
 
        # paramiko seems to generate detailless errors.
327
 
        if (e.errno == errno.ENOENT or
328
 
            e.args == ('No such file or directory',) or
329
 
            e.args == ('No such file',)):
330
 
            raise NoSuchFile(relpath)
331
 
        if (e.args == ('mkdir failed',)):
332
 
            raise FileExists(relpath)
333
 
        # strange but true, for the paramiko server.
334
 
        if (e.args == ('Failure',)):
335
 
            raise FileExists(relpath)
336
 
        raise
337
 
 
338
 
    def append(self, relpath, f):
339
 
        """
340
 
        Append the text in the file-like object into the final
341
 
        location.
342
 
        """
343
 
        try:
344
 
            path = self._abspath(relpath)
345
 
            fout = self._sftp.file(path, 'ab')
346
 
            self._pump(f, fout)
347
 
        except (IOError, paramiko.SSHException), x:
348
 
            raise SFTPTransportError('Unable to append file %r' % (path,), x)
349
 
 
350
 
    def copy(self, rel_from, rel_to):
351
 
        """Copy the item at rel_from to the location at rel_to"""
352
 
        path_from = self._abspath(rel_from)
353
 
        path_to = self._abspath(rel_to)
354
 
        try:
355
 
            fin = self._sftp.file(path_from, 'rb')
356
 
            try:
357
 
                fout = self._sftp.file(path_to, 'wb')
358
 
                try:
359
 
                    fout.set_pipelined(True)
360
 
                    self._pump(fin, fout)
361
 
                finally:
362
 
                    fout.close()
363
 
            finally:
364
 
                fin.close()
365
 
        except (IOError, paramiko.SSHException), x:
366
 
            raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
367
 
 
368
 
    def move(self, rel_from, rel_to):
369
 
        """Move the item at rel_from to the location at rel_to"""
370
 
        path_from = self._abspath(rel_from)
371
 
        path_to = self._abspath(rel_to)
372
 
        try:
373
 
            self._sftp.rename(path_from, path_to)
374
 
        except (IOError, paramiko.SSHException), x:
375
 
            raise SFTPTransportError('Unable to move %r to %r' % (path_from, path_to), x)
376
 
 
377
 
    def delete(self, relpath):
378
 
        """Delete the item at relpath"""
379
 
        path = self._abspath(relpath)
380
 
        try:
381
 
            self._sftp.remove(path)
382
 
        except (IOError, paramiko.SSHException), x:
383
 
            raise SFTPTransportError('Unable to delete %r' % (path,), x)
384
 
            
385
 
    def listable(self):
386
 
        """Return True if this store supports listing."""
387
 
        return True
388
 
 
389
 
    def list_dir(self, relpath):
390
 
        """
391
 
        Return a list of all files at the given location.
392
 
        """
393
 
        # does anything actually use this?
394
 
        path = self._abspath(relpath)
395
 
        try:
396
 
            return self._sftp.listdir(path)
397
 
        except (IOError, paramiko.SSHException), x:
398
 
            raise SFTPTransportError('Unable to list folder %r' % (path,), x)
399
 
 
400
 
    def stat(self, relpath):
401
 
        """Return the stat information for a file."""
402
 
        path = self._abspath(relpath)
403
 
        try:
404
 
            return self._sftp.stat(path)
405
 
        except (IOError, paramiko.SSHException), x:
406
 
            raise SFTPTransportError('Unable to stat %r' % (path,), x)
407
 
 
408
 
    def lock_read(self, relpath):
409
 
        """
410
 
        Lock the given file for shared (read) access.
411
 
        :return: A lock object, which has an unlock() member function
412
 
        """
413
 
        # FIXME: there should be something clever i can do here...
414
 
        class BogusLock(object):
415
 
            def __init__(self, path):
416
 
                self.path = path
417
 
            def unlock(self):
418
 
                pass
419
 
        return BogusLock(relpath)
420
 
 
421
 
    def lock_write(self, relpath):
422
 
        """
423
 
        Lock the given file for exclusive (write) access.
424
 
        WARNING: many transports do not support this, so trying avoid using it
425
 
 
426
 
        :return: A lock object, which has an unlock() member function
427
 
        """
428
 
        # This is a little bit bogus, but basically, we create a file
429
 
        # which should not already exist, and if it does, we assume
430
 
        # that there is a lock, and if it doesn't, the we assume
431
 
        # that we have taken the lock.
432
 
        return SFTPLock(relpath, self)
433
 
 
434
 
 
435
 
    def _unparse_url(self, path=None):
436
 
        if path is None:
437
 
            path = self._path
438
 
        host = self._host
439
 
        username = urllib.quote(self._username)
440
 
        if self._password:
441
 
            username += ':' + urllib.quote(self._password)
442
 
        if self._port != 22:
443
 
            host += ':%d' % self._port
444
 
        return 'sftp://%s@%s/%s' % (username, host, urllib.quote(path))
445
 
 
446
 
    def _parse_url(self, url):
447
 
        assert url[:7] == 'sftp://'
448
 
        m = self._url_matcher.match(url)
449
 
        if m is None:
450
 
            raise SFTPTransportError('Unable to parse SFTP URL %r' % (url,))
451
 
        self._username, self._password, self._host, self._port, self._path = m.groups()
452
 
        if self._username is None:
453
 
            self._username = getpass.getuser()
454
 
        else:
455
 
            if self._password:
456
 
                # username field is 'user:pass@' in this case, and password is ':pass'
457
 
                username_len = len(self._username) - len(self._password) - 1
458
 
                self._username = urllib.unquote(self._username[:username_len])
459
 
                self._password = urllib.unquote(self._password[1:])
460
 
            else:
461
 
                self._username = urllib.unquote(self._username[:-1])
462
 
        if self._port is None:
463
 
            self._port = 22
464
 
        else:
465
 
            self._port = int(self._port[1:])
466
 
        if (self._path is None) or (self._path == ''):
467
 
            self._path = ''
468
 
        else:
469
 
            # remove leading '/'
470
 
            self._path = urllib.unquote(self._path[1:])
471
 
 
472
 
    def _sftp_connect(self):
473
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
474
 
        
475
 
        load_host_keys()
476
 
        
477
 
        try:
478
 
            t = paramiko.Transport((self._host, self._port))
479
 
            t.start_client()
480
 
        except paramiko.SSHException:
481
 
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
482
 
            
483
 
        server_key = t.get_remote_server_key()
484
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
485
 
        keytype = server_key.get_name()
486
 
        if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
487
 
            our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
488
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
489
 
        elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
490
 
            our_server_key = BZR_HOSTKEYS[self._host][keytype]
491
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
492
 
        else:
493
 
            warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
494
 
            if not BZR_HOSTKEYS.has_key(self._host):
495
 
                BZR_HOSTKEYS[self._host] = {}
496
 
            BZR_HOSTKEYS[self._host][keytype] = server_key
497
 
            our_server_key = server_key
498
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
499
 
            save_host_keys()
500
 
        if server_key != our_server_key:
501
 
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
502
 
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
503
 
            raise SFTPTransportError('Host keys for %s do not match!  %s != %s' % \
504
 
                (self._host, our_server_key_hex, server_key_hex),
505
 
                ['Try editing %s or %s' % (filename1, filename2)])
506
 
 
507
 
        self._sftp_auth(t, self._username, self._host)
508
 
        
509
 
        try:
510
 
            self._sftp = t.open_sftp_client()
511
 
        except paramiko.SSHException:
512
 
            raise BzrError('Unable to find path %s on SFTP server %s' % \
513
 
                (self._path, self._host))
514
 
 
515
 
    def _sftp_auth(self, transport, username, host):
516
 
        agent = paramiko.Agent()
517
 
        for key in agent.get_keys():
518
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
519
 
            try:
520
 
                transport.auth_publickey(self._username, key)
521
 
                return
522
 
            except paramiko.SSHException, e:
523
 
                pass
524
 
        
525
 
        # okay, try finding id_rsa or id_dss?  (posix only)
526
 
        if self._try_pkey_auth(transport, paramiko.RSAKey, 'id_rsa'):
527
 
            return
528
 
        if self._try_pkey_auth(transport, paramiko.DSSKey, 'id_dsa'):
529
 
            return
530
 
 
531
 
        if self._password:
532
 
            try:
533
 
                transport.auth_password(self._username, self._password)
534
 
                return
535
 
            except paramiko.SSHException, e:
536
 
                pass
537
 
 
538
 
        # give up and ask for a password
539
 
        # FIXME: shouldn't be implementing UI this deep into bzrlib
540
 
        enc = sys.stdout.encoding
541
 
        password = getpass.getpass('SSH %s@%s password: ' %
542
 
            (self._username.encode(enc, 'replace'), self._host.encode(enc, 'replace')))
543
 
        try:
544
 
            transport.auth_password(self._username, password)
545
 
        except paramiko.SSHException:
546
 
            raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
547
 
                (self._username, self._host))
548
 
 
549
 
    def _try_pkey_auth(self, transport, pkey_class, filename):
550
 
        filename = os.path.expanduser('~/.ssh/' + filename)
551
 
        try:
552
 
            key = pkey_class.from_private_key_file(filename)
553
 
            transport.auth_publickey(self._username, key)
554
 
            return True
555
 
        except paramiko.PasswordRequiredException:
556
 
            # FIXME: shouldn't be implementing UI this deep into bzrlib
557
 
            enc = sys.stdout.encoding
558
 
            password = getpass.getpass('SSH %s password: ' % 
559
 
                (os.path.basename(filename).encode(enc, 'replace'),))
560
 
            try:
561
 
                key = pkey_class.from_private_key_file(filename, password)
562
 
                transport.auth_publickey(self._username, key)
563
 
                return True
564
 
            except paramiko.SSHException:
565
 
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
566
 
        except paramiko.SSHException:
567
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
568
 
        except IOError:
569
 
            pass
570
 
        return False
571
 
 
572
 
    def _sftp_open_exclusive(self, relpath):
573
 
        """Open a remote path exclusively.
574
 
 
575
 
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
576
 
        the file already exists. However it does not expose this
577
 
        at the higher level of SFTPClient.open(), so we have to
578
 
        sneak away with it.
579
 
 
580
 
        WARNING: This breaks the SFTPClient abstraction, so it
581
 
        could easily break against an updated version of paramiko.
582
 
 
583
 
        :param relpath: The relative path, where the file should be opened
584
 
        """
585
 
        path = self._abspath(relpath)
586
 
        attr = SFTPAttributes()
587
 
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
588
 
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
589
 
        try:
590
 
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
591
 
            if t != CMD_HANDLE:
592
 
                raise SFTPTransportError('Expected an SFTP handle')
593
 
            handle = msg.get_string()
594
 
            return SFTPFile(self._sftp, handle, 'w', -1)
595
 
        except IOError, e:
596
 
            self._translate_io_exception(e, relpath)
597
 
        except paramiko.SSHException, x:
598
 
            raise SFTPTransportError('Unable to open file %r' % (path,), x)
599