133
class SFTPTransport(ConnectedTransport):
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):
134
200
"""Transport implementation for SFTP access."""
136
202
_do_prefetch = _default_do_prefetch
151
217
# up the request itself, rather than us having to worry about it
152
218
_max_request_size = 32768
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
220
def __init__(self, base, clone_from=None):
221
super(SFTPTransport, self).__init__(base)
222
if clone_from is None:
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)
225
# use the same ssh connection, etc
226
self._sftp = clone_from._sftp
227
# super saves 'self.base'
209
229
def should_cache(self):
211
231
Return True if the data pulled across should be cached locally.
235
def clone(self, offset=None):
237
Return a new SFTPTransport with root at self.base + offset.
238
We share the same SFTP session between such transports, because it's
239
fairly expensive to set them up.
242
return SFTPTransport(self.base, self)
244
return SFTPTransport(self.abspath(offset), self)
246
def _remote_path(self, relpath):
247
"""Return the path to be passed along the sftp protocol for relpath.
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 /~/.
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
299
assert isinstance(relpath, basestring)
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)
355
def relpath(self, abspath):
356
scheme, username, password, host, port, path = self._split_url(abspath)
358
if (username != self._username):
359
error.append('username mismatch')
360
if (host != self._host):
361
error.append('host mismatch')
362
if (port != self._port):
363
error.append('port mismatch')
364
if (not path.startswith(self._path)):
365
error.append('path mismatch')
367
extra = ': ' + ', '.join(error)
368
raise PathNotChild(abspath, self.base, extra=extra)
370
return path[pl:].strip('/')
215
372
def has(self, relpath):
217
374
Does the target location exist?
220
self._get_sftp().stat(self._remote_path(relpath))
377
self._sftp.stat(self._remote_path(relpath))