122
class SFTPTransport(Transport):
137
class SFTPUrlHandling(Transport):
138
"""Mix-in that does common handling of SSH/SFTP URLs."""
140
def __init__(self, base):
141
self._parse_url(base)
142
base = self._unparse_url(self._path)
145
super(SFTPUrlHandling, self).__init__(base)
147
def _parse_url(self, url):
149
self._username, self._password,
150
self._host, self._port, self._path) = self._split_url(url)
152
def _unparse_url(self, path):
153
"""Return a URL for a path relative to this transport.
155
path = urllib.quote(path)
156
# handle homedir paths
157
if not path.startswith('/'):
159
netloc = urllib.quote(self._host)
160
if self._username is not None:
161
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
162
if self._port is not None:
163
netloc = '%s:%d' % (netloc, self._port)
164
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
166
def _split_url(self, url):
167
(scheme, username, password, host, port, path) = split_url(url)
168
## assert scheme == 'sftp'
170
# the initial slash should be removed from the path, and treated
171
# as a homedir relative path (the path begins with a double slash
172
# if it is absolute).
173
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
174
# RBC 20060118 we are not using this as its too user hostile. instead
175
# we are following lftp and using /~/foo to mean '~/foo'.
176
# handle homedir paths
177
if path.startswith('/~/'):
181
return (scheme, username, password, host, port, path)
183
def abspath(self, relpath):
184
"""Return the full url to the given relative path.
186
@param relpath: the relative path or path components
187
@type relpath: str or list
189
return self._unparse_url(self._remote_path(relpath))
191
def _remote_path(self, relpath):
192
"""Return the path to be passed along the sftp protocol for relpath.
194
:param relpath: is a urlencoded string.
196
return self._combine_paths(self._path, relpath)
199
class SFTPTransport(SFTPUrlHandling):
123
200
"""Transport implementation for SFTP access."""
125
202
_do_prefetch = _default_do_prefetch
172
244
return SFTPTransport(self.abspath(offset), self)
174
def abspath(self, relpath):
176
Return the full url to the given relative path.
178
@param relpath: the relative path or path components
179
@type relpath: str or list
181
return self._unparse_url(self._remote_path(relpath))
183
246
def _remote_path(self, relpath):
184
247
"""Return the path to be passed along the sftp protocol for relpath.
186
249
relpath is a urlencoded string.
251
:return: a path prefixed with / for regular abspath-based urls, or a
252
path that does not begin with / for urls which begin with /~/.
188
# FIXME: share the common code across transports
254
# how does this work?
255
# it processes relpath with respect to
257
# firstly we create a path to evaluate:
258
# if relpath is an abspath or homedir path, its the entire thing
259
# otherwise we join our base with relpath
260
# then we eliminate all empty segments (double //'s) outside the first
261
# two elements of the list. This avoids problems with trailing
262
# slashes, or other abnormalities.
263
# finally we evaluate the entire path in a single pass
265
# '..' result in popping the left most already
266
# processed path (which can never be empty because of the check for
267
# abspath and homedir meaning that its not, or that we've used our
268
# path. If the pop would pop the root, we ignore it.
270
# Specific case examinations:
271
# remove the special casefor ~: if the current root is ~/ popping of it
272
# = / thus our seed for a ~ based path is ['', '~']
273
# and if we end up with [''] then we had basically ('', '..') (which is
274
# '/..' so we append '' if the length is one, and assert that the first
275
# element is still ''. Lastly, if we end with ['', '~'] as a prefix for
276
# the output, we've got a homedir path, so we strip that prefix before
277
# '/' joining the resulting list.
279
# case one: '/' -> ['', ''] cannot shrink
280
# case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
281
# and pop the second '' for the '..', append 'foo'
282
# case three: '/~/' -> ['', '~', '']
283
# case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
284
# and we want to get '/foo' - the empty path in the middle
285
# needs to be stripped, then normal path manipulation will
287
# case five: '/..' ['', '..'], we want ['', '']
288
# stripping '' outside the first two is ok
289
# ignore .. if its too high up
291
# lastly this code is possibly reusable by FTP, but not reusable by
292
# local paths: ~ is resolvable correctly, nor by HTTP or the smart
293
# server: ~ is resolved remotely.
295
# however, a version of this that acts on self.base is possible to be
296
# written which manipulates the URL in canonical form, and would be
297
# reusable for all transports, if a flag for allowing ~/ at all was
189
299
assert isinstance(relpath, basestring)
190
relpath = urlutils.unescape(relpath).split('/')
191
basepath = self._path.split('/')
192
if len(basepath) > 0 and basepath[-1] == '':
193
basepath = basepath[:-1]
197
if len(basepath) == 0:
198
# In most filesystems, a request for the parent
199
# of root, just returns root.
207
path = '/'.join(basepath)
208
# mutter('relpath => remotepath %s => %s', relpath, path)
300
relpath = urlutils.unescape(relpath)
303
if relpath.startswith('/'):
304
# abspath - normal split is fine.
305
current_path = relpath.split('/')
306
elif relpath.startswith('~/'):
307
# root is homedir based: normal split and prefix '' to remote the
309
current_path = [''].extend(relpath.split('/'))
311
# root is from the current directory:
312
if self._path.startswith('/'):
313
# abspath, take the regular split
316
# homedir based, add the '', '~' not present in self._path
317
current_path = ['', '~']
318
# add our current dir
319
current_path.extend(self._path.split('/'))
320
# add the users relpath
321
current_path.extend(relpath.split('/'))
322
# strip '' segments that are not in the first one - the leading /.
323
to_process = current_path[:1]
324
for segment in current_path[1:]:
326
to_process.append(segment)
328
# process '.' and '..' segments into output_path.
330
for segment in to_process:
332
# directory pop. Remove a directory
333
# as long as we are not at the root
334
if len(output_path) > 1:
337
# cannot pop beyond the root, so do nothing
339
continue # strip the '.' from the output.
341
# this will append '' to output_path for the root elements,
342
# which is appropriate: its why we strip '' in the first pass.
343
output_path.append(segment)
345
# check output special cases:
346
if output_path == ['']:
348
output_path = ['', '']
349
elif output_path[:2] == ['', '~']:
350
# ['', '~', ...] -> ...
351
output_path = output_path[2:]
352
path = '/'.join(output_path)
211
355
def relpath(self, abspath):
212
username, password, host, port, path = self._split_url(abspath)
356
scheme, username, password, host, port, path = self._split_url(abspath)
214
358
if (username != self._username):
215
359
error.append('username mismatch')
263
407
fp = self._sftp.file(path, mode='rb')
264
408
readv = getattr(fp, 'readv', None)
266
return self._sftp_readv(fp, offsets)
410
return self._sftp_readv(fp, offsets, relpath)
267
411
mutter('seek and read %s offsets', len(offsets))
268
return self._seek_and_read(fp, offsets)
412
return self._seek_and_read(fp, offsets, relpath)
269
413
except (IOError, paramiko.SSHException), e:
270
414
self._translate_io_exception(e, path, ': error retrieving')
272
def _sftp_readv(self, fp, offsets):
416
def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
273
417
"""Use the readv() member of fp to do async readv.
275
419
And then read them using paramiko.readv(). paramiko.readv()
673
823
# that we have taken the lock.
674
824
return SFTPLock(relpath, self)
676
def _unparse_url(self, path=None):
679
path = urllib.quote(path)
680
# handle homedir paths
681
if not path.startswith('/'):
683
netloc = urllib.quote(self._host)
684
if self._username is not None:
685
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
686
if self._port is not None:
687
netloc = '%s:%d' % (netloc, self._port)
688
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
690
def _split_url(self, url):
691
(scheme, username, password, host, port, path) = split_url(url)
692
assert scheme == 'sftp'
694
# the initial slash should be removed from the path, and treated
695
# as a homedir relative path (the path begins with a double slash
696
# if it is absolute).
697
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
698
# RBC 20060118 we are not using this as its too user hostile. instead
699
# we are following lftp and using /~/foo to mean '~/foo'.
700
# handle homedir paths
701
if path.startswith('/~/'):
705
return (username, password, host, port, path)
707
def _parse_url(self, url):
708
(self._username, self._password,
709
self._host, self._port, self._path) = self._split_url(url)
711
826
def _sftp_connect(self):
712
827
"""Connect to the remote sftp server.
713
828
After this, self._sftp should have a valid connection (or