1
# Copyright (C) 2005-2010 Canonical Ltd
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.
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.
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Implementation of Transport over SFTP, using paramiko."""
19
from __future__ import absolute_import
21
# TODO: Remove the transport-based lock_read and lock_write methods. They'll
22
# then raise TransportNotPossible, which will break remote access to any
23
# formats which rely on OS-level locks. That should be fine as those formats
24
# are pretty old, but these combinations may have to be removed from the test
25
# suite. Those formats all date back to 0.7; so we should be able to remove
26
# these methods when we officially drop support for those formats.
44
from bzrlib.errors import (FileExists,
51
from bzrlib.osutils import fancy_rename
52
from bzrlib.trace import mutter, warning
53
from bzrlib.transport import (
60
# Disable one particular warning that comes from paramiko in Python2.5; if
61
# this is emitted at the wrong time it tends to cause spurious test failures
62
# or at least noise in the test case::
64
# [1770/7639 in 86s, 1 known failures, 50 skipped, 2 missing features]
65
# test_permissions.TestSftpPermissions.test_new_files
66
# /var/lib/python-support/python2.5/paramiko/message.py:226: DeprecationWarning: integer argument expected, got float
67
# self.packet.write(struct.pack('>I', n))
68
warnings.filterwarnings('ignore',
69
'integer argument expected, got float',
70
category=DeprecationWarning,
71
module='paramiko.message')
75
except ImportError, e:
76
raise ParamikoNotPresent(e)
78
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
79
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
80
SFTP_OK, CMD_HANDLE, CMD_OPEN)
81
from paramiko.sftp_attr import SFTPAttributes
82
from paramiko.sftp_file import SFTPFile
85
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
86
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
87
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
90
class SFTPLock(object):
91
"""This fakes a lock in a remote location.
93
A present lock is indicated just by the existence of a file. This
94
doesn't work well on all transports and they are only used in
95
deprecated storage formats.
98
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
100
def __init__(self, path, transport):
101
self.lock_file = None
103
self.lock_path = path + '.write-lock'
104
self.transport = transport
106
# RBC 20060103 FIXME should we be using private methods here ?
107
abspath = transport._remote_path(self.lock_path)
108
self.lock_file = transport._sftp_open_exclusive(abspath)
110
raise LockError('File %r already locked' % (self.path,))
113
if not self.lock_file:
115
self.lock_file.close()
116
self.lock_file = None
118
self.transport.delete(self.lock_path)
119
except (NoSuchFile,):
120
# What specific errors should we catch here?
124
class _SFTPReadvHelper(object):
125
"""A class to help with managing the state of a readv request."""
127
# See _get_requests for an explanation.
128
_max_request_size = 32768
130
def __init__(self, original_offsets, relpath, _report_activity):
131
"""Create a new readv helper.
133
:param original_offsets: The original requests given by the caller of
135
:param relpath: The name of the file (if known)
136
:param _report_activity: A Transport._report_activity bound method,
137
to be called as data arrives.
139
self.original_offsets = list(original_offsets)
140
self.relpath = relpath
141
self._report_activity = _report_activity
143
def _get_requests(self):
144
"""Break up the offsets into individual requests over sftp.
146
The SFTP spec only requires implementers to support 32kB requests. We
147
could try something larger (openssh supports 64kB), but then we have to
148
handle requests that fail.
149
So instead, we just break up our maximum chunks into 32kB chunks, and
150
asyncronously requests them.
151
Newer versions of paramiko would do the chunking for us, but we want to
152
start processing results right away, so we do it ourselves.
154
# TODO: Because we issue async requests, we don't 'fudge' any extra
155
# data. I'm not 100% sure that is the best choice.
157
# The first thing we do, is to collapse the individual requests as much
158
# as possible, so we don't issues requests <32kB
159
sorted_offsets = sorted(self.original_offsets)
160
coalesced = list(ConnectedTransport._coalesce_offsets(sorted_offsets,
161
limit=0, fudge_factor=0))
163
for c_offset in coalesced:
164
start = c_offset.start
165
size = c_offset.length
167
# Break this up into 32kB requests
169
next_size = min(size, self._max_request_size)
170
requests.append((start, next_size))
173
if 'sftp' in debug.debug_flags:
174
mutter('SFTP.readv(%s) %s offsets => %s coalesced => %s requests',
175
self.relpath, len(sorted_offsets), len(coalesced),
179
def request_and_yield_offsets(self, fp):
180
"""Request the data from the remote machine, yielding the results.
182
:param fp: A Paramiko SFTPFile object that supports readv.
183
:return: Yield the data requested by the original readv caller, one by
186
requests = self._get_requests()
187
offset_iter = iter(self.original_offsets)
188
cur_offset, cur_size = offset_iter.next()
189
# paramiko .readv() yields strings that are in the order of the requests
190
# So we track the current request to know where the next data is
191
# being returned from.
197
# This is used to buffer chunks which we couldn't process yet
198
# It is (start, end, data) tuples.
200
# Create an 'unlimited' data stream, so we stop based on requests,
201
# rather than just because the data stream ended. This lets us detect
203
data_stream = itertools.chain(fp.readv(requests),
204
itertools.repeat(None))
205
for (start, length), data in itertools.izip(requests, data_stream):
207
if cur_coalesced is not None:
208
raise errors.ShortReadvError(self.relpath,
209
start, length, len(data))
210
if len(data) != length:
211
raise errors.ShortReadvError(self.relpath,
212
start, length, len(data))
213
self._report_activity(length, 'read')
215
# This is the first request, just buffer it
216
buffered_data = [data]
217
buffered_len = length
219
elif start == last_end:
220
# The data we are reading fits neatly on the previous
221
# buffer, so this is all part of a larger coalesced range.
222
buffered_data.append(data)
223
buffered_len += length
225
# We have an 'interrupt' in the data stream. So we know we are
226
# at a request boundary.
228
# We haven't consumed the buffer so far, so put it into
229
# data_chunks, and continue.
230
buffered = ''.join(buffered_data)
231
data_chunks.append((input_start, buffered))
233
buffered_data = [data]
234
buffered_len = length
235
last_end = start + length
236
if input_start == cur_offset and cur_size <= buffered_len:
237
# Simplify the next steps a bit by transforming buffered_data
238
# into a single string. We also have the nice property that
239
# when there is only one string ''.join([x]) == x, so there is
241
buffered = ''.join(buffered_data)
242
# Clean out buffered data so that we keep memory
246
# TODO: We *could* also consider the case where cur_offset is in
247
# in the buffered range, even though it doesn't *start*
248
# the buffered range. But for packs we pretty much always
249
# read in order, so you won't get any extra data in the
251
while (input_start == cur_offset
252
and (buffered_offset + cur_size) <= buffered_len):
253
# We've buffered enough data to process this request, spit it
255
cur_data = buffered[buffered_offset:buffered_offset + cur_size]
256
# move the direct pointer into our buffered data
257
buffered_offset += cur_size
258
# Move the start-of-buffer pointer
259
input_start += cur_size
260
# Yield the requested data
261
yield cur_offset, cur_data
262
cur_offset, cur_size = offset_iter.next()
263
# at this point, we've consumed as much of buffered as we can,
264
# so break off the portion that we consumed
265
if buffered_offset == len(buffered_data):
266
# No tail to leave behind
270
buffered = buffered[buffered_offset:]
271
buffered_data = [buffered]
272
buffered_len = len(buffered)
273
# now that the data stream is done, close the handle
276
buffered = ''.join(buffered_data)
278
data_chunks.append((input_start, buffered))
280
if 'sftp' in debug.debug_flags:
281
mutter('SFTP readv left with %d out-of-order bytes',
282
sum(map(lambda x: len(x[1]), data_chunks)))
283
# We've processed all the readv data, at this point, anything we
284
# couldn't process is in data_chunks. This doesn't happen often, so
285
# this code path isn't optimized
286
# We use an interesting process for data_chunks
287
# Specifically if we have "bisect_left([(start, len, entries)],
289
# If start == qstart, then we get the specific node. Otherwise we
290
# get the previous node
292
idx = bisect.bisect_left(data_chunks, (cur_offset,))
293
if idx < len(data_chunks) and data_chunks[idx][0] == cur_offset:
294
# The data starts here
295
data = data_chunks[idx][1][:cur_size]
297
# The data is in a portion of a previous page
299
sub_offset = cur_offset - data_chunks[idx][0]
300
data = data_chunks[idx][1]
301
data = data[sub_offset:sub_offset + cur_size]
303
# We are missing the page where the data should be found,
306
if len(data) != cur_size:
307
raise AssertionError('We must have miscalulated.'
308
' We expected %d bytes, but only found %d'
309
% (cur_size, len(data)))
310
yield cur_offset, data
311
cur_offset, cur_size = offset_iter.next()
314
class SFTPTransport(ConnectedTransport):
315
"""Transport implementation for SFTP access."""
317
_do_prefetch = _default_do_prefetch
318
# TODO: jam 20060717 Conceivably these could be configurable, either
319
# by auto-tuning at run-time, or by a configuration (per host??)
320
# but the performance curve is pretty flat, so just going with
321
# reasonable defaults.
322
_max_readv_combine = 200
323
# Having to round trip to the server means waiting for a response,
324
# so it is better to download extra bytes.
325
# 8KiB had good performance for both local and remote network operations
326
_bytes_to_read_before_seek = 8192
328
# The sftp spec says that implementations SHOULD allow reads
329
# to be at least 32K. paramiko.readv() does an async request
330
# for the chunks. So we need to keep it within a single request
331
# size for paramiko <= 1.6.1. paramiko 1.6.2 will probably chop
332
# up the request itself, rather than us having to worry about it
333
_max_request_size = 32768
335
def _remote_path(self, relpath):
336
"""Return the path to be passed along the sftp protocol for relpath.
338
:param relpath: is a urlencoded string.
340
remote_path = self._parsed_url.clone(relpath).path
341
# the initial slash should be removed from the path, and treated as a
342
# homedir relative path (the path begins with a double slash if it is
343
# absolute). see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
344
# RBC 20060118 we are not using this as its too user hostile. instead
345
# we are following lftp and using /~/foo to mean '~/foo'
346
# vila--20070602 and leave absolute paths begin with a single slash.
347
if remote_path.startswith('/~/'):
348
remote_path = remote_path[3:]
349
elif remote_path == '/~':
353
def _create_connection(self, credentials=None):
354
"""Create a new connection with the provided credentials.
356
:param credentials: The credentials needed to establish the connection.
358
:return: The created connection and its associated credentials.
360
The credentials are only the password as it may have been entered
361
interactively by the user and may be different from the one provided
362
in base url at transport creation time.
364
if credentials is None:
365
password = self._parsed_url.password
367
password = credentials
369
vendor = ssh._get_ssh_vendor()
370
user = self._parsed_url.user
372
auth = config.AuthenticationConfig()
373
user = auth.get_user('ssh', self._parsed_url.host,
374
self._parsed_url.port)
375
connection = vendor.connect_sftp(self._parsed_url.user, password,
376
self._parsed_url.host, self._parsed_url.port)
377
return connection, (user, password)
379
def disconnect(self):
380
connection = self._get_connection()
381
if connection is not None:
385
"""Ensures that a connection is established"""
386
connection = self._get_connection()
387
if connection is None:
388
# First connection ever
389
connection, credentials = self._create_connection()
390
self._set_connection(connection, credentials)
393
def has(self, relpath):
395
Does the target location exist?
398
self._get_sftp().stat(self._remote_path(relpath))
399
# stat result is about 20 bytes, let's say
400
self._report_activity(20, 'read')
405
def get(self, relpath):
406
"""Get the file at the given relative path.
408
:param relpath: The relative path to the file
411
path = self._remote_path(relpath)
412
f = self._get_sftp().file(path, mode='rb')
413
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
416
except (IOError, paramiko.SSHException), e:
417
self._translate_io_exception(e, path, ': error retrieving',
418
failure_exc=errors.ReadError)
420
def get_bytes(self, relpath):
421
# reimplement this here so that we can report how many bytes came back
422
f = self.get(relpath)
425
self._report_activity(len(bytes), 'read')
430
def _readv(self, relpath, offsets):
431
"""See Transport.readv()"""
432
# We overload the default readv() because we want to use a file
433
# that does not have prefetch enabled.
434
# Also, if we have a new paramiko, it implements an async readv()
439
path = self._remote_path(relpath)
440
fp = self._get_sftp().file(path, mode='rb')
441
readv = getattr(fp, 'readv', None)
443
return self._sftp_readv(fp, offsets, relpath)
444
if 'sftp' in debug.debug_flags:
445
mutter('seek and read %s offsets', len(offsets))
446
return self._seek_and_read(fp, offsets, relpath)
447
except (IOError, paramiko.SSHException), e:
448
self._translate_io_exception(e, path, ': error retrieving')
450
def recommended_page_size(self):
451
"""See Transport.recommended_page_size().
453
For SFTP we suggest a large page size to reduce the overhead
454
introduced by latency.
458
def _sftp_readv(self, fp, offsets, relpath):
459
"""Use the readv() member of fp to do async readv.
461
Then read them using paramiko.readv(). paramiko.readv()
462
does not support ranges > 64K, so it caps the request size, and
463
just reads until it gets all the stuff it wants.
465
helper = _SFTPReadvHelper(offsets, relpath, self._report_activity)
466
return helper.request_and_yield_offsets(fp)
468
def put_file(self, relpath, f, mode=None):
470
Copy the file-like object into the location.
472
:param relpath: Location to put the contents, relative to base.
473
:param f: File-like object.
474
:param mode: The final mode for the file
476
final_path = self._remote_path(relpath)
477
return self._put(final_path, f, mode=mode)
479
def _put(self, abspath, f, mode=None):
480
"""Helper function so both put() and copy_abspaths can reuse the code"""
481
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
482
os.getpid(), random.randint(0,0x7FFFFFFF))
483
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
487
fout.set_pipelined(True)
488
length = self._pump(f, fout)
489
except (IOError, paramiko.SSHException), e:
490
self._translate_io_exception(e, tmp_abspath)
491
# XXX: This doesn't truly help like we would like it to.
492
# The problem is that openssh strips sticky bits. So while we
493
# can properly set group write permission, we lose the group
494
# sticky bit. So it is probably best to stop chmodding, and
495
# just tell users that they need to set the umask correctly.
496
# The attr.st_mode = mode, in _sftp_open_exclusive
497
# will handle when the user wants the final mode to be more
498
# restrictive. And then we avoid a round trip. Unless
499
# paramiko decides to expose an async chmod()
501
# This is designed to chmod() right before we close.
502
# Because we set_pipelined() earlier, theoretically we might
503
# avoid the round trip for fout.close()
505
self._get_sftp().chmod(tmp_abspath, mode)
508
self._rename_and_overwrite(tmp_abspath, abspath)
511
# If we fail, try to clean up the temporary file
512
# before we throw the exception
513
# but don't let another exception mess things up
514
# Write out the traceback, because otherwise
515
# the catch and throw destroys it
517
mutter(traceback.format_exc())
521
self._get_sftp().remove(tmp_abspath)
523
# raise the saved except
525
# raise the original with its traceback if we can.
528
def _put_non_atomic_helper(self, relpath, writer, mode=None,
529
create_parent_dir=False,
531
abspath = self._remote_path(relpath)
533
# TODO: jam 20060816 paramiko doesn't publicly expose a way to
534
# set the file mode at create time. If it does, use it.
535
# But for now, we just chmod later anyway.
537
def _open_and_write_file():
538
"""Try to open the target file, raise error on failure"""
542
fout = self._get_sftp().file(abspath, mode='wb')
543
fout.set_pipelined(True)
545
except (paramiko.SSHException, IOError), e:
546
self._translate_io_exception(e, abspath,
549
# This is designed to chmod() right before we close.
550
# Because we set_pipelined() earlier, theoretically we might
551
# avoid the round trip for fout.close()
553
self._get_sftp().chmod(abspath, mode)
558
if not create_parent_dir:
559
_open_and_write_file()
562
# Try error handling to create the parent directory if we need to
564
_open_and_write_file()
566
# Try to create the parent directory, and then go back to
568
parent_dir = os.path.dirname(abspath)
569
self._mkdir(parent_dir, dir_mode)
570
_open_and_write_file()
572
def put_file_non_atomic(self, relpath, f, mode=None,
573
create_parent_dir=False,
575
"""Copy the file-like object into the target location.
577
This function is not strictly safe to use. It is only meant to
578
be used when you already know that the target does not exist.
579
It is not safe, because it will open and truncate the remote
580
file. So there may be a time when the file has invalid contents.
582
:param relpath: The remote location to put the contents.
583
:param f: File-like object.
584
:param mode: Possible access permissions for new file.
585
None means do not set remote permissions.
586
:param create_parent_dir: If we cannot create the target file because
587
the parent directory does not exist, go ahead and
588
create it, and then try again.
592
self._put_non_atomic_helper(relpath, writer, mode=mode,
593
create_parent_dir=create_parent_dir,
596
def put_bytes_non_atomic(self, relpath, bytes, mode=None,
597
create_parent_dir=False,
601
self._put_non_atomic_helper(relpath, writer, mode=mode,
602
create_parent_dir=create_parent_dir,
605
def iter_files_recursive(self):
606
"""Walk the relative paths of all files in this transport."""
607
# progress is handled by list_dir
608
queue = list(self.list_dir('.'))
610
relpath = queue.pop(0)
611
st = self.stat(relpath)
612
if stat.S_ISDIR(st.st_mode):
613
for i, basename in enumerate(self.list_dir(relpath)):
614
queue.insert(i, relpath+'/'+basename)
618
def _mkdir(self, abspath, mode=None):
624
self._report_activity(len(abspath), 'write')
625
self._get_sftp().mkdir(abspath, local_mode)
626
self._report_activity(1, 'read')
628
# chmod a dir through sftp will erase any sgid bit set
629
# on the server side. So, if the bit mode are already
630
# set, avoid the chmod. If the mode is not fine but
631
# the sgid bit is set, report a warning to the user
632
# with the umask fix.
633
stat = self._get_sftp().lstat(abspath)
634
mode = mode & 0777 # can't set special bits anyway
635
if mode != stat.st_mode & 0777:
636
if stat.st_mode & 06000:
637
warning('About to chmod %s over sftp, which will result'
638
' in its suid or sgid bits being cleared. If'
639
' you want to preserve those bits, change your '
640
' environment on the server to use umask 0%03o.'
641
% (abspath, 0777 - mode))
642
self._get_sftp().chmod(abspath, mode=mode)
643
except (paramiko.SSHException, IOError), e:
644
self._translate_io_exception(e, abspath, ': unable to mkdir',
645
failure_exc=FileExists)
647
def mkdir(self, relpath, mode=None):
648
"""Create a directory at the given path."""
649
self._mkdir(self._remote_path(relpath), mode=mode)
651
def open_write_stream(self, relpath, mode=None):
652
"""See Transport.open_write_stream."""
653
# initialise the file to zero-length
654
# this is three round trips, but we don't use this
655
# api more than once per write_group at the moment so
656
# it is a tolerable overhead. Better would be to truncate
657
# the file after opening. RBC 20070805
658
self.put_bytes_non_atomic(relpath, "", mode)
659
abspath = self._remote_path(relpath)
660
# TODO: jam 20060816 paramiko doesn't publicly expose a way to
661
# set the file mode at create time. If it does, use it.
662
# But for now, we just chmod later anyway.
665
handle = self._get_sftp().file(abspath, mode='wb')
666
handle.set_pipelined(True)
667
except (paramiko.SSHException, IOError), e:
668
self._translate_io_exception(e, abspath,
670
_file_streams[self.abspath(relpath)] = handle
671
return FileFileStream(self, relpath, handle)
673
def _translate_io_exception(self, e, path, more_info='',
674
failure_exc=PathError):
675
"""Translate a paramiko or IOError into a friendlier exception.
677
:param e: The original exception
678
:param path: The path in question when the error is raised
679
:param more_info: Extra information that can be included,
680
such as what was going on
681
:param failure_exc: Paramiko has the super fun ability to raise completely
682
opaque errors that just set "e.args = ('Failure',)" with
684
If this parameter is set, it defines the exception
685
to raise in these cases.
687
# paramiko seems to generate detailless errors.
688
self._translate_error(e, path, raise_generic=False)
689
if getattr(e, 'args', None) is not None:
690
if (e.args == ('No such file or directory',) or
691
e.args == ('No such file',)):
692
raise NoSuchFile(path, str(e) + more_info)
693
if (e.args == ('mkdir failed',) or
694
e.args[0].startswith('syserr: File exists')):
695
raise FileExists(path, str(e) + more_info)
696
# strange but true, for the paramiko server.
697
if (e.args == ('Failure',)):
698
raise failure_exc(path, str(e) + more_info)
699
# Can be something like args = ('Directory not empty:
700
# '/srv/bazaar.launchpad.net/blah...: '
701
# [Errno 39] Directory not empty',)
702
if (e.args[0].startswith('Directory not empty: ')
703
or getattr(e, 'errno', None) == errno.ENOTEMPTY):
704
raise errors.DirectoryNotEmpty(path, str(e))
705
if e.args == ('Operation unsupported',):
706
raise errors.TransportNotPossible()
707
mutter('Raising exception with args %s', e.args)
708
if getattr(e, 'errno', None) is not None:
709
mutter('Raising exception with errno %s', e.errno)
712
def append_file(self, relpath, f, mode=None):
714
Append the text in the file-like object into the final
718
path = self._remote_path(relpath)
719
fout = self._get_sftp().file(path, 'ab')
721
self._get_sftp().chmod(path, mode)
725
except (IOError, paramiko.SSHException), e:
726
self._translate_io_exception(e, relpath, ': unable to append')
728
def rename(self, rel_from, rel_to):
729
"""Rename without special overwriting"""
731
self._get_sftp().rename(self._remote_path(rel_from),
732
self._remote_path(rel_to))
733
except (IOError, paramiko.SSHException), e:
734
self._translate_io_exception(e, rel_from,
735
': unable to rename to %r' % (rel_to))
737
def _rename_and_overwrite(self, abs_from, abs_to):
738
"""Do a fancy rename on the remote server.
740
Using the implementation provided by osutils.
743
sftp = self._get_sftp()
744
fancy_rename(abs_from, abs_to,
745
rename_func=sftp.rename,
746
unlink_func=sftp.remove)
747
except (IOError, paramiko.SSHException), e:
748
self._translate_io_exception(e, abs_from,
749
': unable to rename to %r' % (abs_to))
751
def move(self, rel_from, rel_to):
752
"""Move the item at rel_from to the location at rel_to"""
753
path_from = self._remote_path(rel_from)
754
path_to = self._remote_path(rel_to)
755
self._rename_and_overwrite(path_from, path_to)
757
def delete(self, relpath):
758
"""Delete the item at relpath"""
759
path = self._remote_path(relpath)
761
self._get_sftp().remove(path)
762
except (IOError, paramiko.SSHException), e:
763
self._translate_io_exception(e, path, ': unable to delete')
765
def external_url(self):
766
"""See bzrlib.transport.Transport.external_url."""
767
# the external path for SFTP is the base
771
"""Return True if this store supports listing."""
774
def list_dir(self, relpath):
776
Return a list of all files at the given location.
778
# does anything actually use this?
780
# This is at least used by copy_tree for remote upgrades.
781
# -- David Allouche 2006-08-11
782
path = self._remote_path(relpath)
784
entries = self._get_sftp().listdir(path)
785
self._report_activity(sum(map(len, entries)), 'read')
786
except (IOError, paramiko.SSHException), e:
787
self._translate_io_exception(e, path, ': failed to list_dir')
788
return [urlutils.escape(entry) for entry in entries]
790
def rmdir(self, relpath):
791
"""See Transport.rmdir."""
792
path = self._remote_path(relpath)
794
return self._get_sftp().rmdir(path)
795
except (IOError, paramiko.SSHException), e:
796
self._translate_io_exception(e, path, ': failed to rmdir')
798
def stat(self, relpath):
799
"""Return the stat information for a file."""
800
path = self._remote_path(relpath)
802
return self._get_sftp().lstat(path)
803
except (IOError, paramiko.SSHException), e:
804
self._translate_io_exception(e, path, ': unable to stat')
806
def readlink(self, relpath):
807
"""See Transport.readlink."""
808
path = self._remote_path(relpath)
810
return self._get_sftp().readlink(path)
811
except (IOError, paramiko.SSHException), e:
812
self._translate_io_exception(e, path, ': unable to readlink')
814
def symlink(self, source, link_name):
815
"""See Transport.symlink."""
817
conn = self._get_sftp()
818
sftp_retval = conn.symlink(source, link_name)
819
if SFTP_OK != sftp_retval:
820
raise TransportError(
821
'%r: unable to create symlink to %r' % (link_name, source),
824
except (IOError, paramiko.SSHException), e:
825
self._translate_io_exception(e, link_name,
826
': unable to create symlink to %r' % (source))
828
def lock_read(self, relpath):
830
Lock the given file for shared (read) access.
831
:return: A lock object, which has an unlock() member function
833
# FIXME: there should be something clever i can do here...
834
class BogusLock(object):
835
def __init__(self, path):
839
return BogusLock(relpath)
841
def lock_write(self, relpath):
843
Lock the given file for exclusive (write) access.
844
WARNING: many transports do not support this, so trying avoid using it
846
:return: A lock object, which has an unlock() member function
848
# This is a little bit bogus, but basically, we create a file
849
# which should not already exist, and if it does, we assume
850
# that there is a lock, and if it doesn't, the we assume
851
# that we have taken the lock.
852
return SFTPLock(relpath, self)
854
def _sftp_open_exclusive(self, abspath, mode=None):
855
"""Open a remote path exclusively.
857
SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
858
the file already exists. However it does not expose this
859
at the higher level of SFTPClient.open(), so we have to
862
WARNING: This breaks the SFTPClient abstraction, so it
863
could easily break against an updated version of paramiko.
865
:param abspath: The remote absolute path where the file should be opened
866
:param mode: The mode permissions bits for the new file
868
# TODO: jam 20060816 Paramiko >= 1.6.2 (probably earlier) supports
869
# using the 'x' flag to indicate SFTP_FLAG_EXCL.
870
# However, there is no way to set the permission mode at open
871
# time using the sftp_client.file() functionality.
872
path = self._get_sftp()._adjust_cwd(abspath)
873
# mutter('sftp abspath %s => %s', abspath, path)
874
attr = SFTPAttributes()
877
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
878
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
880
t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
882
raise TransportError('Expected an SFTP handle')
883
handle = msg.get_string()
884
return SFTPFile(self._get_sftp(), handle, 'wb', -1)
885
except (paramiko.SSHException, IOError), e:
886
self._translate_io_exception(e, abspath, ': unable to open',
887
failure_exc=FileExists)
889
def _can_roundtrip_unix_modebits(self):
890
if sys.platform == 'win32':
897
def get_test_permutations():
898
"""Return the permutations to be used in testing."""
899
from bzrlib.tests import stub_sftp
900
return [(SFTPTransport, stub_sftp.SFTPAbsoluteServer),
901
(SFTPTransport, stub_sftp.SFTPHomeDirServer),
902
(SFTPTransport, stub_sftp.SFTPSiblingAbsoluteServer),