136
class SFTPTransport(Transport):
134
class SFTPUrlHandling(Transport):
135
"""Mix-in that does common handling of SSH/SFTP URLs."""
137
def __init__(self, base):
138
self._parse_url(base)
139
base = self._unparse_url(self._path)
142
super(SFTPUrlHandling, self).__init__(base)
144
def _parse_url(self, url):
146
self._username, self._password,
147
self._host, self._port, self._path) = self._split_url(url)
149
def _unparse_url(self, path):
150
"""Return a URL for a path relative to this transport.
152
path = urllib.quote(path)
153
# handle homedir paths
154
if not path.startswith('/'):
156
netloc = urllib.quote(self._host)
157
if self._username is not None:
158
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
159
if self._port is not None:
160
netloc = '%s:%d' % (netloc, self._port)
161
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
163
def _split_url(self, url):
164
(scheme, username, password, host, port, path) = split_url(url)
165
## assert scheme == 'sftp'
167
# the initial slash should be removed from the path, and treated
168
# as a homedir relative path (the path begins with a double slash
169
# if it is absolute).
170
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
171
# RBC 20060118 we are not using this as its too user hostile. instead
172
# we are following lftp and using /~/foo to mean '~/foo'.
173
# handle homedir paths
174
if path.startswith('/~/'):
178
return (scheme, username, password, host, port, path)
180
def _remote_path(self, relpath):
181
"""Return the path to be passed along the sftp protocol for relpath.
183
:param relpath: is a urlencoded string.
185
return self._combine_paths(self._path, relpath)
188
class SFTPTransport(SFTPUrlHandling):
137
189
"""Transport implementation for SFTP access."""
139
191
_do_prefetch = _default_do_prefetch
198
245
"""Return the path to be passed along the sftp protocol for relpath.
200
247
relpath is a urlencoded string.
249
:return: a path prefixed with / for regular abspath-based urls, or a
250
path that does not begin with / for urls which begin with /~/.
202
# FIXME: share the common code across transports
252
# how does this work?
253
# it processes relpath with respect to
255
# firstly we create a path to evaluate:
256
# if relpath is an abspath or homedir path, its the entire thing
257
# otherwise we join our base with relpath
258
# then we eliminate all empty segments (double //'s) outside the first
259
# two elements of the list. This avoids problems with trailing
260
# slashes, or other abnormalities.
261
# finally we evaluate the entire path in a single pass
263
# '..' result in popping the left most already
264
# processed path (which can never be empty because of the check for
265
# abspath and homedir meaning that its not, or that we've used our
266
# path. If the pop would pop the root, we ignore it.
268
# Specific case examinations:
269
# remove the special casefor ~: if the current root is ~/ popping of it
270
# = / thus our seed for a ~ based path is ['', '~']
271
# and if we end up with [''] then we had basically ('', '..') (which is
272
# '/..' so we append '' if the length is one, and assert that the first
273
# element is still ''. Lastly, if we end with ['', '~'] as a prefix for
274
# the output, we've got a homedir path, so we strip that prefix before
275
# '/' joining the resulting list.
277
# case one: '/' -> ['', ''] cannot shrink
278
# case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
279
# and pop the second '' for the '..', append 'foo'
280
# case three: '/~/' -> ['', '~', '']
281
# case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
282
# and we want to get '/foo' - the empty path in the middle
283
# needs to be stripped, then normal path manipulation will
285
# case five: '/..' ['', '..'], we want ['', '']
286
# stripping '' outside the first two is ok
287
# ignore .. if its too high up
289
# lastly this code is possibly reusable by FTP, but not reusable by
290
# local paths: ~ is resolvable correctly, nor by HTTP or the smart
291
# server: ~ is resolved remotely.
293
# however, a version of this that acts on self.base is possible to be
294
# written which manipulates the URL in canonical form, and would be
295
# reusable for all transports, if a flag for allowing ~/ at all was
203
297
assert isinstance(relpath, basestring)
204
relpath = urlutils.unescape(relpath).split('/')
205
basepath = self._path.split('/')
206
if len(basepath) > 0 and basepath[-1] == '':
207
basepath = basepath[:-1]
211
if len(basepath) == 0:
212
# In most filesystems, a request for the parent
213
# of root, just returns root.
221
path = '/'.join(basepath)
222
# mutter('relpath => remotepath %s => %s', relpath, path)
298
relpath = urlutils.unescape(relpath)
301
if relpath.startswith('/'):
302
# abspath - normal split is fine.
303
current_path = relpath.split('/')
304
elif relpath.startswith('~/'):
305
# root is homedir based: normal split and prefix '' to remote the
307
current_path = [''].extend(relpath.split('/'))
309
# root is from the current directory:
310
if self._path.startswith('/'):
311
# abspath, take the regular split
314
# homedir based, add the '', '~' not present in self._path
315
current_path = ['', '~']
316
# add our current dir
317
current_path.extend(self._path.split('/'))
318
# add the users relpath
319
current_path.extend(relpath.split('/'))
320
# strip '' segments that are not in the first one - the leading /.
321
to_process = current_path[:1]
322
for segment in current_path[1:]:
324
to_process.append(segment)
326
# process '.' and '..' segments into output_path.
328
for segment in to_process:
330
# directory pop. Remove a directory
331
# as long as we are not at the root
332
if len(output_path) > 1:
335
# cannot pop beyond the root, so do nothing
337
continue # strip the '.' from the output.
339
# this will append '' to output_path for the root elements,
340
# which is appropriate: its why we strip '' in the first pass.
341
output_path.append(segment)
343
# check output special cases:
344
if output_path == ['']:
346
output_path = ['', '']
347
elif output_path[:2] == ['', '~']:
348
# ['', '~', ...] -> ...
349
output_path = output_path[2:]
350
path = '/'.join(output_path)
225
353
def relpath(self, abspath):
226
username, password, host, port, path = self._split_url(abspath)
354
scheme, username, password, host, port, path = self._split_url(abspath)
228
356
if (username != self._username):
229
357
error.append('username mismatch')
687
815
# that we have taken the lock.
688
816
return SFTPLock(relpath, self)
690
def _unparse_url(self, path=None):
693
path = urllib.quote(path)
694
# handle homedir paths
695
if not path.startswith('/'):
697
netloc = urllib.quote(self._host)
698
if self._username is not None:
699
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
700
if self._port is not None:
701
netloc = '%s:%d' % (netloc, self._port)
702
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
704
def _split_url(self, url):
705
(scheme, username, password, host, port, path) = split_url(url)
706
assert scheme == 'sftp'
708
# the initial slash should be removed from the path, and treated
709
# as a homedir relative path (the path begins with a double slash
710
# if it is absolute).
711
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
712
# RBC 20060118 we are not using this as its too user hostile. instead
713
# we are following lftp and using /~/foo to mean '~/foo'.
714
# handle homedir paths
715
if path.startswith('/~/'):
719
return (username, password, host, port, path)
721
def _parse_url(self, url):
722
(self._username, self._password,
723
self._host, self._port, self._path) = self._split_url(url)
725
818
def _sftp_connect(self):
726
819
"""Connect to the remote sftp server.
727
820
After this, self._sftp should have a valid connection (or