138
class SFTPUrlHandling(Transport):
139
"""Mix-in that does common handling of SSH/SFTP URLs."""
141
def __init__(self, base):
142
self._parse_url(base)
143
base = self._unparse_url(self._path)
146
super(SFTPUrlHandling, self).__init__(base)
148
def _parse_url(self, url):
150
self._username, self._password,
151
self._host, self._port, self._path) = self._split_url(url)
153
def _unparse_url(self, path):
154
"""Return a URL for a path relative to this transport.
156
path = urllib.quote(path)
157
# handle homedir paths
158
if not path.startswith('/'):
160
netloc = urllib.quote(self._host)
161
if self._username is not None:
162
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
163
if self._port is not None:
164
netloc = '%s:%d' % (netloc, self._port)
165
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
167
def _split_url(self, url):
168
(scheme, username, password, host, port, path) = split_url(url)
169
## assert scheme == 'sftp'
171
# the initial slash should be removed from the path, and treated
172
# as a homedir relative path (the path begins with a double slash
173
# if it is absolute).
174
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
175
# RBC 20060118 we are not using this as its too user hostile. instead
176
# we are following lftp and using /~/foo to mean '~/foo'.
177
# handle homedir paths
178
if path.startswith('/~/'):
182
return (scheme, username, password, host, port, path)
184
def abspath(self, relpath):
185
"""Return the full url to the given relative path.
187
@param relpath: the relative path or path components
188
@type relpath: str or list
190
return self._unparse_url(self._remote_path(relpath))
192
def _remote_path(self, relpath):
193
"""Return the path to be passed along the sftp protocol for relpath.
195
:param relpath: is a urlencoded string.
197
return self._combine_paths(self._path, relpath)
200
class SFTPTransport(SFTPUrlHandling):
133
class SFTPTransport(ConnectedTransport):
201
134
"""Transport implementation for SFTP access."""
203
136
_do_prefetch = _default_do_prefetch
218
151
# up the request itself, rather than us having to worry about it
219
152
_max_request_size = 32768
221
def __init__(self, base, clone_from=None):
222
super(SFTPTransport, self).__init__(base)
223
if clone_from is None:
154
def __init__(self, base, _from_transport=None):
155
assert base.startswith('sftp://')
156
super(SFTPTransport, self).__init__(base,
157
_from_transport=_from_transport)
159
def _remote_path(self, relpath):
160
"""Return the path to be passed along the sftp protocol for relpath.
162
:param relpath: is a urlencoded string.
164
relative = urlutils.unescape(relpath).encode('utf-8')
165
remote_path = self._combine_paths(self._path, relative)
166
# the initial slash should be removed from the path, and treated as a
167
# homedir relative path (the path begins with a double slash if it is
168
# absolute). see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
169
# RBC 20060118 we are not using this as its too user hostile. instead
170
# we are following lftp and using /~/foo to mean '~/foo'
171
# vila--20070602 and leave absolute paths begin with a single slash.
172
if remote_path.startswith('/~/'):
173
remote_path = remote_path[3:]
174
elif remote_path == '/~':
178
def _create_connection(self, credentials=None):
179
"""Create a new connection with the provided credentials.
181
:param credentials: The credentials needed to establish the connection.
183
:return: The created connection and its associated credentials.
185
The credentials are only the password as it may have been entered
186
interactively by the user and may be different from the one provided
187
in base url at transport creation time.
189
if credentials is None:
190
password = self._password
226
# use the same ssh connection, etc
227
self._sftp = clone_from._sftp
228
# super saves 'self.base'
192
password = credentials
194
vendor = ssh._get_ssh_vendor()
195
connection = vendor.connect_sftp(self._user, password,
196
self._host, self._port)
197
return connection, password
200
"""Ensures that a connection is established"""
201
connection = self._get_connection()
202
if connection is None:
203
# First connection ever
204
connection, credentials = self._create_connection()
205
self._set_connection(connection, credentials)
230
209
def should_cache(self):
232
211
Return True if the data pulled across should be cached locally.
236
def clone(self, offset=None):
238
Return a new SFTPTransport with root at self.base + offset.
239
We share the same SFTP session between such transports, because it's
240
fairly expensive to set them up.
243
return SFTPTransport(self.base, self)
245
return SFTPTransport(self.abspath(offset), self)
247
def _remote_path(self, relpath):
248
"""Return the path to be passed along the sftp protocol for relpath.
250
relpath is a urlencoded string.
252
:return: a path prefixed with / for regular abspath-based urls, or a
253
path that does not begin with / for urls which begin with /~/.
255
# how does this work?
256
# it processes relpath with respect to
258
# firstly we create a path to evaluate:
259
# if relpath is an abspath or homedir path, its the entire thing
260
# otherwise we join our base with relpath
261
# then we eliminate all empty segments (double //'s) outside the first
262
# two elements of the list. This avoids problems with trailing
263
# slashes, or other abnormalities.
264
# finally we evaluate the entire path in a single pass
266
# '..' result in popping the left most already
267
# processed path (which can never be empty because of the check for
268
# abspath and homedir meaning that its not, or that we've used our
269
# path. If the pop would pop the root, we ignore it.
271
# Specific case examinations:
272
# remove the special casefor ~: if the current root is ~/ popping of it
273
# = / thus our seed for a ~ based path is ['', '~']
274
# and if we end up with [''] then we had basically ('', '..') (which is
275
# '/..' so we append '' if the length is one, and assert that the first
276
# element is still ''. Lastly, if we end with ['', '~'] as a prefix for
277
# the output, we've got a homedir path, so we strip that prefix before
278
# '/' joining the resulting list.
280
# case one: '/' -> ['', ''] cannot shrink
281
# case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
282
# and pop the second '' for the '..', append 'foo'
283
# case three: '/~/' -> ['', '~', '']
284
# case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
285
# and we want to get '/foo' - the empty path in the middle
286
# needs to be stripped, then normal path manipulation will
288
# case five: '/..' ['', '..'], we want ['', '']
289
# stripping '' outside the first two is ok
290
# ignore .. if its too high up
292
# lastly this code is possibly reusable by FTP, but not reusable by
293
# local paths: ~ is resolvable correctly, nor by HTTP or the smart
294
# server: ~ is resolved remotely.
296
# however, a version of this that acts on self.base is possible to be
297
# written which manipulates the URL in canonical form, and would be
298
# reusable for all transports, if a flag for allowing ~/ at all was
300
assert isinstance(relpath, basestring)
301
relpath = urlutils.unescape(relpath)
304
if relpath.startswith('/'):
305
# abspath - normal split is fine.
306
current_path = relpath.split('/')
307
elif relpath.startswith('~/'):
308
# root is homedir based: normal split and prefix '' to remote the
310
current_path = [''].extend(relpath.split('/'))
312
# root is from the current directory:
313
if self._path.startswith('/'):
314
# abspath, take the regular split
317
# homedir based, add the '', '~' not present in self._path
318
current_path = ['', '~']
319
# add our current dir
320
current_path.extend(self._path.split('/'))
321
# add the users relpath
322
current_path.extend(relpath.split('/'))
323
# strip '' segments that are not in the first one - the leading /.
324
to_process = current_path[:1]
325
for segment in current_path[1:]:
327
to_process.append(segment)
329
# process '.' and '..' segments into output_path.
331
for segment in to_process:
333
# directory pop. Remove a directory
334
# as long as we are not at the root
335
if len(output_path) > 1:
338
# cannot pop beyond the root, so do nothing
340
continue # strip the '.' from the output.
342
# this will append '' to output_path for the root elements,
343
# which is appropriate: its why we strip '' in the first pass.
344
output_path.append(segment)
346
# check output special cases:
347
if output_path == ['']:
349
output_path = ['', '']
350
elif output_path[:2] == ['', '~']:
351
# ['', '~', ...] -> ...
352
output_path = output_path[2:]
353
path = '/'.join(output_path)
356
def relpath(self, abspath):
357
scheme, username, password, host, port, path = self._split_url(abspath)
359
if (username != self._username):
360
error.append('username mismatch')
361
if (host != self._host):
362
error.append('host mismatch')
363
if (port != self._port):
364
error.append('port mismatch')
365
if (not path.startswith(self._path)):
366
error.append('path mismatch')
368
extra = ': ' + ', '.join(error)
369
raise PathNotChild(abspath, self.base, extra=extra)
371
return path[pl:].strip('/')
373
215
def has(self, relpath):
375
217
Does the target location exist?
378
self._sftp.stat(self._remote_path(relpath))
220
self._get_sftp().stat(self._remote_path(relpath))