~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] Added the uncommit plugin

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