[Checkins] SVN: zc.buildoutsftp/branches/jim-dev/ checkpoint
jim
cvs-admin at zope.org
Thu Jun 28 15:48:01 UTC 2012
Log message for revision 127143:
checkpoint
Changed:
U zc.buildoutsftp/branches/jim-dev/buildout.cfg
U zc.buildoutsftp/branches/jim-dev/setup.py
U zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py
D zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/buildoutsftp.py
A zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test
A zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py
D zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/urllib2sftp.py
-=-
Modified: zc.buildoutsftp/branches/jim-dev/buildout.cfg
===================================================================
--- zc.buildoutsftp/branches/jim-dev/buildout.cfg 2012-06-28 15:45:59 UTC (rev 127142)
+++ zc.buildoutsftp/branches/jim-dev/buildout.cfg 2012-06-28 15:47:58 UTC (rev 127143)
@@ -2,9 +2,13 @@
develop = .
parts = py
+[test]
+recipe = zc.recipe.testrunner
+eggs = zc.buildoutsftp [test]
+
[py]
recipe = zc.recipe.egg
-eggs = zc.buildoutsftp
+eggs = ${test:eggs}
interpreter = py
Modified: zc.buildoutsftp/branches/jim-dev/setup.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/setup.py 2012-06-28 15:45:59 UTC (rev 127142)
+++ zc.buildoutsftp/branches/jim-dev/setup.py 2012-06-28 15:47:58 UTC (rev 127143)
@@ -17,6 +17,7 @@
package_dir = {'':'src'},
namespace_packages = ['zc'],
install_requires = ['paramiko', 'setuptools'],
+ extras_require = dict(test=['zope.testing', 'mock']),
zip_safe=False,
entry_points = {
'zc.buildout.extension': ['default = %s.buildoutsftp:install' % name],
Modified: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py 2012-06-28 15:45:59 UTC (rev 127142)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/__init__.py 2012-06-28 15:47:58 UTC (rev 127143)
@@ -1 +1,259 @@
+##############################################################################
#
+# Copyright (c) 2006, 2012 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import atexit
+import cStringIO
+import getpass
+import logging
+import logging
+import mimetypes
+import os
+import paramiko
+import re
+import stat
+import sys
+import urllib
+import urllib2
+
+logger = logging.getLogger(__name__)
+
+def install(buildout=None):
+ urllib2.install_opener(urllib2.build_opener(SFTPHandler))
+ logging.getLogger('paramiko').setLevel(logger.getEffectiveLevel()+10)
+
+def unload(buildout=None):
+ # no uninstall_opener. Screw it. :)
+ cleanup()
+
+parse_url_host = re.compile(
+ '(?:' '([^@:]+)(?::([^@]*))?@' ')?'
+ '([^:]*)(?::(\d+))?$').match
+
+_configs = None
+def _get_config(host):
+ global _configs
+ if _configs is None:
+ _configs = []
+ for path in ('/etc/ssh/ssh_config', '/etc/ssh/ssh_config',
+ os.path.expanduser('~/.ssh/config')):
+ if os.path.exists(path):
+ config = paramiko.SSHConfig()
+ with open(path) as f:
+ config.parse(f)
+ _configs.append(config)
+
+ r = {}
+ for config in _configs:
+ r.update(config.lookup(host))
+
+ return r
+
+if sys.platform == 'win32':
+ import _winreg
+ parse_reg_key_name = re.compile('(rsa|dss)2?@22:(\S+)$').match
+ def _get_host_keys(config):
+ regkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
+ r'Software\SimonTatham\PuTTY\SshHostKeys',
+ )
+ keys = paramiko.HostKeys()
+ i = 0
+ while 1:
+ try:
+ name, value, type_ = _winreg.EnumValue(regkey, i)
+ i += 1
+ value = [long(v, 16) for v in value.split(',')]
+ ktype, host = parse_reg_key_name(name).groups()
+ if ktype == 'rsa':
+ key = paramiko.RSAKey(vals=value)
+ if ktype == 'dss':
+ key = paramiko.DSSKey(vals=value)
+ keys.add(host, 'ssh-'+ktype, key)
+ except WindowsError:
+ break
+
+ return keys
+
+else:
+ def _get_host_keys(config):
+ user_host_keys = os.path.expanduser('~/.ssh/known_hosts')
+ if os.path.exists(user_host_keys):
+ host_keys = paramiko.HostKeys(user_host_keys)
+ else:
+ host_keys = {}
+ global_host_keys = config.get('GlobalKnownHostsFile')
+ if not global_host_keys:
+ for path in ('/etc/ssh/GlobalKnownHostsFile',
+ '/etc/ssh_known_hosts'):
+ if os.path.exists(path):
+ global_host_keys = path
+ break
+ if global_host_keys:
+ host_keys.update(paramiko.HostKeys(user_host_keys))
+ return host_keys
+
+class Result:
+
+ def __init__(self, fp, url, info, trans):
+ self._fp = fp
+ self.url = url
+ self.headers = info
+ self.__trans = trans
+
+ def geturl(self):
+ return self.url
+
+ def info(self):
+ return self.headers
+
+ def __getattr__(self, name):
+ return getattr(self._fp, name)
+
+def _open_key(key_path):
+ key = None
+ if os.path.exists(key_path):
+ try:
+ key = paramiko.RSAKey.from_private_key_file(key_path)
+ except paramiko.SSHException:
+ try:
+ key = paramiko.DSSKey.from_private_key_file(key_path)
+ except paramiko.SSHException:
+ logger.error('Invalid key file: %s', key_path)
+ return key
+
+_connection_pool = {}
+
+def cleanup():
+ for k in list(_connection_pool):
+ trans = _connection_pool.pop(k)
+ if trans is not False:
+ trans.close()
+
+atexit.register(cleanup)
+
+class SFTPHandler(urllib2.BaseHandler):
+
+ def sftp_open(self, req):
+ host = req.get_host()
+ if not host:
+ raise IOError, ('sftp error', 'no host given')
+
+ parsed = parse_url_host(host)
+ if not parsed:
+ raise IOError, ('sftp error', 'invalid host', host)
+
+ user, pw, host, port = parsed.groups()
+
+ host = urllib.unquote(host or '')
+
+ config = _get_config(host)
+
+ host_keys = _get_host_keys(config).get(host)
+ if host_keys is None:
+ raise paramiko.AuthenticationException("No stored host key", host)
+
+ if user:
+ user = urllib.unquote(user)
+ else:
+ user = config.get('user', getpass.getuser())
+
+ if port:
+ port = int(port)
+ else:
+ port = 22
+
+ if pw:
+ pw = urllib.unquote(pw)
+
+
+ if pw is not None:
+ pool_key = (host, port, user, pw)
+ trans = _connection_pool.get(pool_key)
+ if trans is None:
+ trans = paramiko.Transport((host, port))
+ try:
+ trans.connect(username=user, password=pw)
+ except paramiko.AuthenticationException:
+ trans.close()
+ raise
+ else:
+ keys = list(paramiko.Agent().get_keys())
+ IdentityFile = config.get('IdentityFile')
+ if IdentityFile:
+ key = _open_key(IdentityFile)
+ if key is None:
+ logger.error('IdentityFile, %s, does not exist',
+ IdentityFile)
+ else:
+ keys.insert(0, key)
+ else:
+ for path in (
+ '~/.ssh/identity', '~/.ssh/id_rsa', '~/.ssh/id_dsa'):
+ key = _open_key(os.path.expanduser(path))
+ if key is not None:
+ keys.insert(0, key)
+
+ for key in keys:
+ pool_key = (host, port, str(key))
+ trans = _connection_pool.get(pool_key)
+ if trans is not None:
+ if trans is False:
+ # Failed previously, so don't try again
+ continue
+ break
+ trans = paramiko.Transport((host, port))
+ try:
+ trans.connect(username=user, pkey=key)
+ break
+ except paramiko.AuthenticationException:
+ trans.close()
+ _connection_pool[pool_key] = False
+ else:
+ raise paramiko.AuthenticationException(
+ "Authentication failed.")
+
+ if pool_key not in _connection_pool:
+ # Check host key
+ remote_server_key = trans.get_remote_server_key()
+ host_key = host_keys.get(remote_server_key.get_name())
+ if host_key != remote_server_key:
+ raise paramiko.AuthenticationException(
+ "Remote server authentication failed.", host)
+ _connection_pool[pool_key] = trans
+
+ sftp = paramiko.SFTPClient.from_transport(trans)
+
+ path = req.get_selector()
+ url = req.get_full_url()
+ logger.debug('sftp get: %s', url)
+ mode = sftp.stat(path).st_mode
+ if stat.S_ISDIR(mode):
+ if logger.getEffectiveLevel() < logging.DEBUG:
+ logger.log(1, "Dir %s:\n %s\n",
+ path, '\n '.join(sftp.listdir(path)))
+
+ return Result(
+ cStringIO.StringIO('\n'.join([
+ ('<a href="%s/%s">%s</a><br />'
+ % (url, x, x)
+ )
+ for x in sftp.listdir(path)
+ ])),
+ url, {'content-type': 'text/html'}, trans)
+ else:
+ mtype = mimetypes.guess_type(url)[0]
+ if mtype is None:
+ mtype = 'application/octet-stream'
+ return Result(sftp.open(path), url, {'content-type': mtype},
+ trans)
+
Deleted: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/buildoutsftp.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/buildoutsftp.py 2012-06-28 15:45:59 UTC (rev 127142)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/buildoutsftp.py 2012-06-28 15:47:58 UTC (rev 127143)
@@ -1,28 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2006 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-
-import logging
-import urllib2
-import zc.buildoutsftp.urllib2sftp
-
-def install(buildout=None):
- urllib2.install_opener(
- urllib2.build_opener(zc.buildoutsftp.urllib2sftp.SFTPHandler)
- )
- logging.getLogger('paramiko').setLevel(
- logging.getLogger().getEffectiveLevel()+10)
-
-def unload(buildout=None):
- # no uninstall_opener. Screw it. :)
- zc.buildoutsftp.urllib2sftp.cleanup()
Added: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test (rev 0)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/main.test 2012-06-28 15:47:58 UTC (rev 127143)
@@ -0,0 +1,49 @@
+Mochiavellian tests
+===================
+
+We're not going to try to test actuall sftp interaction. Too hard.
+Instead, we'll mock the environmet and back up the mock-based tests
+with integration tests.
+
+The buildoutsftp extension installs an sftp handler in urllib2.
+
+ >>> import zc.buildoutsftp, urllib2
+ >>> zc.buildoutsftp.install(None)
+ >>> urllib2.build_opener.assert_called_with(zc.buildoutsftp.SFTPHandler)
+ >>> urllib2.install_opener.assert_called_with(
+ ... urllib2.build_opener.return_value)
+
+A buildout object is passed to install, but install ignores it.
+
+ >>> handler = zc.buildoutsftp.SFTPHandler()
+ >>> request = urllib2.Request('sftp://example.com')
+
+Let's start with essentially no ssh support in the user's environment:
+
+ >>> handler.sftp_open(request)
+ Traceback (most recent call last):
+ ...
+ AuthenticationException: ('No stored host key', 'example.com')
+
+There's a early check for the presense of host keys that failed.
+We'll create an empty host key file:
+
+ >>> import os, paramiko
+ >>> host_key = paramiko.RSAKey.generate(1024)
+ >>> host_keys = paramiko.HostKeys()
+ >>> host_keys.add('example.com', 'ssh-rsa', host_key)
+ >>> os.mkdir('.ssh', 0700)
+ >>> host_keys.save(os.path.join('.ssh', 'known_hosts'))
+
+ >>> handler.sftp_open(request)
+ Traceback (most recent call last):
+ ...
+ AuthenticationException: Authentication failed.
+
+Authentication failed because the user has no keys.
+
+Let's give them one:
+
+ >>> ukey = creds[(('example.com', 22), 'testuser')]['user_key']
+ >>> ukey.write_private_key_file(os.path.join('.ssh', 'id_rsa'))
+
Added: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py (rev 0)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/tests.py 2012-06-28 15:47:58 UTC (rev 127143)
@@ -0,0 +1,130 @@
+##############################################################################
+#
+# Copyright (c) Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+from zope.testing import setupstack
+import doctest
+import mock
+import os
+import paramiko
+import unittest
+
+def side_effect(mock, f=None):
+ if f is None:
+ return lambda f: side_effect(mock, f)
+ mock.side_effect = f
+
+def hack_path(path):
+ if path.startswith('/etc/'):
+ return path[1:]
+ return path
+
+def setup(test):
+ globs = test.globs
+ setupstack.setUpDirectory(test)
+ setupstack.context_manager(test, mock.patch('urllib2.install_opener'))
+ setupstack.context_manager(test, mock.patch('urllib2.build_opener'))
+ setupstack.context_manager(
+ test, mock.patch.dict(os.environ, values=dict(HOME=os.getcwd())))
+
+ original_exists = os.path.exists
+ @side_effect(setupstack.context_manager(test, mock.patch('os.path.exists')))
+ def exists(path):
+ return original_exists(hack_path(path))
+
+ original_open = open
+ setupstack.context_manager(
+ test, mock.patch.dict(__builtins__, values=dict(open=mock.MagicMock())))
+ @side_effect(__builtins__['open'])
+ def _open(path, *args, **kw):
+ return original_open(hack_path(path), *args, **kw)
+
+ setupstack.context_manager(test, mock.patch('getpass.getuser')
+ ).return_value = 'testuser'
+
+ globs['agent_keys'] = agent_keys = []
+ @side_effect(
+ setupstack.context_manager(
+ test, mock.patch('paramiko.Agent')).return_value.get_keys)
+ def get_keys():
+ return agent_keys
+
+ globs['creds'] = creds = {
+ # {(addr, user} -> dict(host_key, user_key}
+ (('example.com', 22), 'testuser'): dict(
+ host_key = paramiko.RSAKey.generate(1024),
+ user_key = paramiko.RSAKey.generate(1024),
+ ),
+ }
+
+ class Transport:
+
+ def __init__(self, addr):
+ self.addr = addr
+
+ def connect(self, username, pkey):
+ if pkey != creds.get((self.addr, username))['user_key']:
+ raise paramiko.AuthenticationException()
+ self.username = username
+
+ def close(self):
+ pass
+
+ def get_remote_server_key(self):
+ return creds.get((self.addr, self.username))['user_key']
+
+ @side_effect(
+ setupstack.context_manager(
+ test, mock.patch('paramiko.Transport')))
+ def transport(addr):
+ return Transport(addr)
+
+ globs['server_files'] = server_files = {} # addr -> path
+ class SFTPClient:
+
+ def __init__(self, transport):
+ self.addr = transport.addr
+
+ def _path(self, path):
+ assert path.startswith(os.path.sep)
+ base = '%s:%s' % self.addr
+ path = path[1:]
+ if path:
+ return os.path.join(base, path)
+ else:
+ return base
+
+ def stat(path):
+ return os.stat(self._path(path))
+
+ def listdir(self):
+ return os.listdir(self._path(path))
+
+ def open(self, path):
+ return original_open(self._path(path))
+
+ @side_effect(
+ setupstack.context_manager(
+ test, mock.patch('paramiko.SFTPClient.from_transport')))
+ def from_transport(trans):
+ return SFTPClient(addr)
+
+ os.makedirs(os.path.join('example.com:22', 'data', 'moredata'))
+ with open(os.path.join('example.com:22', 'data', 'index.html'), 'w') as f:
+ f.write('Hi world!\n')
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite(
+ 'main.test', setUp=setup, tearDown=setupstack.tearDown),
+ ))
+
Deleted: zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/urllib2sftp.py
===================================================================
--- zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/urllib2sftp.py 2012-06-28 15:45:59 UTC (rev 127142)
+++ zc.buildoutsftp/branches/jim-dev/src/zc/buildoutsftp/urllib2sftp.py 2012-06-28 15:47:58 UTC (rev 127143)
@@ -1,190 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2005 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""SFTP Handler for urllib2
-
-$Id$
-"""
-
-import atexit, cStringIO, getpass, logging, mimetypes, os, re, stat, sys
-import urllib, urllib2
-import paramiko
-
-logger = logging.getLogger(__name__)
-
-parse_url_host = re.compile(
- '(?:' '([^@:]+)(?::([^@]*))?@' ')?'
- '([^:]*)(?::(\d+))?$').match
-
-if sys.platform == 'win32':
- import _winreg
- parse_reg_key_name = re.compile('(rsa|dss)2?@22:(\S+)$').match
- def _get_hosts_keys():
- regkey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER,
- r'Software\SimonTatham\PuTTY\SshHostKeys',
- )
- keys = paramiko.HostKeys()
- i = 0
- while 1:
- try:
- name, value, type_ = _winreg.EnumValue(regkey, i)
- i += 1
- value = [long(v, 16) for v in value.split(',')]
- ktype, host = parse_reg_key_name(name).groups()
- if ktype == 'rsa':
- key = paramiko.RSAKey(vals=value)
- if ktype == 'dss':
- key = paramiko.DSSKey(vals=value)
- keys.add(host, 'ssh-'+ktype, key)
- except WindowsError:
- break
-
- return keys
-
-else:
-
- def _get_hosts_keys():
- return paramiko.HostKeys(os.path.expanduser('~/.ssh/known_hosts'))
-
-
-class Result:
-
- def __init__(self, fp, url, info, trans):
- self._fp = fp
- self.url = url
- self.headers = info
- self.__trans = trans
-
- def geturl(self):
- return self.url
-
- def info(self):
- return self.headers
-
- def __getattr__(self, name):
- return getattr(self._fp, name)
-
-_connection_pool = {}
-def cleanup():
- for k in list(_connection_pool):
- trans = _connection_pool.pop(k)
- if trans is not False:
- trans.close()
-
-atexit.register(cleanup)
-
-class SFTPHandler(urllib2.BaseHandler):
-
- def sftp_open(self, req):
- host = req.get_host()
- if not host:
- raise IOError, ('sftp error', 'no host given')
-
- parsed = parse_url_host(host)
- if not parsed:
- raise IOError, ('sftp error', 'invalid host', host)
-
- user, pw, host, port = parsed.groups()
-
- host = urllib.unquote(host or '')
-
- if user:
- user = urllib.unquote(user)
- else:
- user = getpass.getuser()
- config_path = os.path.expanduser('~/.ssh/config')
- if os.path.exists(config_path):
- config = paramiko.SSHConfig()
- config.parse(open(config_path))
- user = config.lookup(host).get('user', user)
-
- if port:
- port = int(port)
- else:
- port = 22
-
- if pw:
- pw = urllib.unquote(pw)
-
-
- host_keys = _get_hosts_keys().get(host)
- if host_keys is None:
- raise paramiko.AuthenticationException(
- "No stored host key", host)
-
- if pw is not None:
- pool_key = (host, port, user, pw)
- trans = _connection_pool.get(pool_key)
- if trans is None:
- trans = paramiko.Transport((host, port))
- try:
- trans.connect(username=user, password=pw)
- except paramiko.AuthenticationException:
- trans.close()
- raise
- else:
- for key in paramiko.Agent().get_keys():
- pool_key = (host, port, str(key))
- trans = _connection_pool.get(pool_key)
- if trans is not None:
- if trans is False:
- # Failed previously, so don't try again
- continue
- break
- trans = paramiko.Transport((host, port))
- try:
- trans.connect(username=user, pkey=key)
- break
- except paramiko.AuthenticationException:
- trans.close()
- _connection_pool[pool_key] = False
- else:
- raise paramiko.AuthenticationException(
- "Authentication failed.")
-
-
- if pool_key not in _connection_pool:
- # Check host key
- remote_server_key = trans.get_remote_server_key()
- host_key = host_keys.get(remote_server_key.get_name())
- if host_key != remote_server_key:
- raise paramiko.AuthenticationException(
- "Remote server authentication failed.", host)
- _connection_pool[pool_key] = trans
-
- sftp = paramiko.SFTPClient.from_transport(trans)
-
- path = req.get_selector()
- url = req.get_full_url()
- logger.debug('sftp get: %s', url)
- mode = sftp.stat(path).st_mode
- if stat.S_ISDIR(mode):
- if logger.getEffectiveLevel() < logging.DEBUG:
- logger.log(1, "Dir %s:\n %s\n",
- path, '\n '.join(sftp.listdir(path)))
-
- return Result(
- cStringIO.StringIO('\n'.join([
- ('<a href="%s/%s">%s</a><br />'
- % (url, x, x)
- )
- for x in sftp.listdir(path)
- ])),
- url, {'content-type': 'text/html'}, trans)
- else:
- mtype = mimetypes.guess_type(url)[0]
- if mtype is None:
- mtype = 'application/octet-stream'
- return Result(sftp.open(path), url, {'content-type': mtype},
- trans)
-
More information about the checkins
mailing list