655 lines
26 KiB
Python
655 lines
26 KiB
Python
|
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
||
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||
|
"""
|
||
|
WSGI applications that parse the URL and dispatch to on-disk resources
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import six
|
||
|
import sys
|
||
|
|
||
|
if six.PY2:
|
||
|
import imp
|
||
|
else:
|
||
|
import importlib.util as imputil
|
||
|
|
||
|
import mimetypes
|
||
|
try:
|
||
|
import pkg_resources
|
||
|
except ImportError:
|
||
|
pkg_resources = None
|
||
|
from paste import request
|
||
|
from paste import fileapp
|
||
|
from paste.util import import_string
|
||
|
from paste import httpexceptions
|
||
|
from .httpheaders import ETAG
|
||
|
from paste.util import converters
|
||
|
|
||
|
class NoDefault(object):
|
||
|
pass
|
||
|
|
||
|
__all__ = ['URLParser', 'StaticURLParser', 'PkgResourcesParser']
|
||
|
|
||
|
class URLParser(object):
|
||
|
|
||
|
"""
|
||
|
WSGI middleware
|
||
|
|
||
|
Application dispatching, based on URL. An instance of `URLParser` is
|
||
|
an application that loads and delegates to other applications. It
|
||
|
looks for files in its directory that match the first part of
|
||
|
PATH_INFO; these may have an extension, but are not required to have
|
||
|
one, in which case the available files are searched to find the
|
||
|
appropriate file. If it is ambiguous, a 404 is returned and an error
|
||
|
logged.
|
||
|
|
||
|
By default there is a constructor for .py files that loads the module,
|
||
|
and looks for an attribute ``application``, which is a ready
|
||
|
application object, or an attribute that matches the module name,
|
||
|
which is a factory for building applications, and is called with no
|
||
|
arguments.
|
||
|
|
||
|
URLParser will also look in __init__.py for special overrides.
|
||
|
These overrides are:
|
||
|
|
||
|
``urlparser_hook(environ)``
|
||
|
This can modify the environment. Its return value is ignored,
|
||
|
and it cannot be used to change the response in any way. You
|
||
|
*can* use this, for example, to manipulate SCRIPT_NAME/PATH_INFO
|
||
|
(try to keep them consistent with the original URL -- but
|
||
|
consuming PATH_INFO and moving that to SCRIPT_NAME is ok).
|
||
|
|
||
|
``urlparser_wrap(environ, start_response, app)``:
|
||
|
After URLParser finds the application, it calls this function
|
||
|
(if present). If this function doesn't call
|
||
|
``app(environ, start_response)`` then the application won't be
|
||
|
called at all! This can be used to allocate resources (with
|
||
|
``try:finally:``) or otherwise filter the output of the
|
||
|
application.
|
||
|
|
||
|
``not_found_hook(environ, start_response)``:
|
||
|
If no file can be found (*in this directory*) to match the
|
||
|
request, then this WSGI application will be called. You can
|
||
|
use this to change the URL and pass the request back to
|
||
|
URLParser again, or on to some other application. This
|
||
|
doesn't catch all ``404 Not Found`` responses, just missing
|
||
|
files.
|
||
|
|
||
|
``application(environ, start_response)``:
|
||
|
This basically overrides URLParser completely, and the given
|
||
|
application is used for all requests. ``urlparser_wrap`` and
|
||
|
``urlparser_hook`` are still called, but the filesystem isn't
|
||
|
searched in any way.
|
||
|
"""
|
||
|
|
||
|
parsers_by_directory = {}
|
||
|
|
||
|
# This is lazily initialized
|
||
|
init_module = NoDefault
|
||
|
|
||
|
global_constructors = {}
|
||
|
|
||
|
def __init__(self, global_conf,
|
||
|
directory, base_python_name,
|
||
|
index_names=NoDefault,
|
||
|
hide_extensions=NoDefault,
|
||
|
ignore_extensions=NoDefault,
|
||
|
constructors=None,
|
||
|
**constructor_conf):
|
||
|
"""
|
||
|
Create a URLParser object that looks at `directory`.
|
||
|
`base_python_name` is the package that this directory
|
||
|
represents, thus any Python modules in this directory will
|
||
|
be given names under this package.
|
||
|
"""
|
||
|
if global_conf:
|
||
|
import warnings
|
||
|
warnings.warn(
|
||
|
'The global_conf argument to URLParser is deprecated; '
|
||
|
'either pass in None or {}, or use make_url_parser',
|
||
|
DeprecationWarning)
|
||
|
else:
|
||
|
global_conf = {}
|
||
|
if os.path.sep != '/':
|
||
|
directory = directory.replace(os.path.sep, '/')
|
||
|
self.directory = directory
|
||
|
self.base_python_name = base_python_name
|
||
|
# This logic here should be deprecated since it is in
|
||
|
# make_url_parser
|
||
|
if index_names is NoDefault:
|
||
|
index_names = global_conf.get(
|
||
|
'index_names', ('index', 'Index', 'main', 'Main'))
|
||
|
self.index_names = converters.aslist(index_names)
|
||
|
if hide_extensions is NoDefault:
|
||
|
hide_extensions = global_conf.get(
|
||
|
'hide_extensions', ('.pyc', '.bak', '.py~', '.pyo'))
|
||
|
self.hide_extensions = converters.aslist(hide_extensions)
|
||
|
if ignore_extensions is NoDefault:
|
||
|
ignore_extensions = global_conf.get(
|
||
|
'ignore_extensions', ())
|
||
|
self.ignore_extensions = converters.aslist(ignore_extensions)
|
||
|
self.constructors = self.global_constructors.copy()
|
||
|
if constructors:
|
||
|
self.constructors.update(constructors)
|
||
|
# @@: Should we also check the global options for constructors?
|
||
|
for name, value in constructor_conf.items():
|
||
|
if not name.startswith('constructor '):
|
||
|
raise ValueError(
|
||
|
"Only extra configuration keys allowed are "
|
||
|
"'constructor .ext = import_expr'; you gave %r "
|
||
|
"(=%r)" % (name, value))
|
||
|
ext = name[len('constructor '):].strip()
|
||
|
if isinstance(value, (str, unicode)):
|
||
|
value = import_string.eval_import(value)
|
||
|
self.constructors[ext] = value
|
||
|
|
||
|
def __call__(self, environ, start_response):
|
||
|
environ['paste.urlparser.base_python_name'] = self.base_python_name
|
||
|
if self.init_module is NoDefault:
|
||
|
self.init_module = self.find_init_module(environ)
|
||
|
path_info = environ.get('PATH_INFO', '')
|
||
|
if not path_info:
|
||
|
return self.add_slash(environ, start_response)
|
||
|
if (self.init_module
|
||
|
and getattr(self.init_module, 'urlparser_hook', None)):
|
||
|
self.init_module.urlparser_hook(environ)
|
||
|
orig_path_info = environ['PATH_INFO']
|
||
|
orig_script_name = environ['SCRIPT_NAME']
|
||
|
application, filename = self.find_application(environ)
|
||
|
if not application:
|
||
|
if (self.init_module
|
||
|
and getattr(self.init_module, 'not_found_hook', None)
|
||
|
and environ.get('paste.urlparser.not_found_parser') is not self):
|
||
|
not_found_hook = self.init_module.not_found_hook
|
||
|
environ['paste.urlparser.not_found_parser'] = self
|
||
|
environ['PATH_INFO'] = orig_path_info
|
||
|
environ['SCRIPT_NAME'] = orig_script_name
|
||
|
return not_found_hook(environ, start_response)
|
||
|
if filename is None:
|
||
|
name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
|
||
|
if not name:
|
||
|
name = 'one of %s' % ', '.join(
|
||
|
self.index_names or
|
||
|
['(no index_names defined)'])
|
||
|
|
||
|
return self.not_found(
|
||
|
environ, start_response,
|
||
|
'Tried to load %s from directory %s'
|
||
|
% (name, self.directory))
|
||
|
else:
|
||
|
environ['wsgi.errors'].write(
|
||
|
'Found resource %s, but could not construct application\n'
|
||
|
% filename)
|
||
|
return self.not_found(
|
||
|
environ, start_response,
|
||
|
'Tried to load %s from directory %s'
|
||
|
% (filename, self.directory))
|
||
|
if (self.init_module
|
||
|
and getattr(self.init_module, 'urlparser_wrap', None)):
|
||
|
return self.init_module.urlparser_wrap(
|
||
|
environ, start_response, application)
|
||
|
else:
|
||
|
return application(environ, start_response)
|
||
|
|
||
|
def find_application(self, environ):
|
||
|
if (self.init_module
|
||
|
and getattr(self.init_module, 'application', None)
|
||
|
and not environ.get('paste.urlparser.init_application') == environ['SCRIPT_NAME']):
|
||
|
environ['paste.urlparser.init_application'] = environ['SCRIPT_NAME']
|
||
|
return self.init_module.application, None
|
||
|
name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
|
||
|
environ['PATH_INFO'] = rest_of_path
|
||
|
if name is not None:
|
||
|
environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + '/' + name
|
||
|
if not name:
|
||
|
names = self.index_names
|
||
|
for index_name in names:
|
||
|
filename = self.find_file(environ, index_name)
|
||
|
if filename:
|
||
|
break
|
||
|
else:
|
||
|
# None of the index files found
|
||
|
filename = None
|
||
|
else:
|
||
|
filename = self.find_file(environ, name)
|
||
|
if filename is None:
|
||
|
return None, filename
|
||
|
else:
|
||
|
return self.get_application(environ, filename), filename
|
||
|
|
||
|
def not_found(self, environ, start_response, debug_message=None):
|
||
|
exc = httpexceptions.HTTPNotFound(
|
||
|
'The resource at %s could not be found'
|
||
|
% request.construct_url(environ),
|
||
|
comment=debug_message)
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def add_slash(self, environ, start_response):
|
||
|
"""
|
||
|
This happens when you try to get to a directory
|
||
|
without a trailing /
|
||
|
"""
|
||
|
url = request.construct_url(environ, with_query_string=False)
|
||
|
url += '/'
|
||
|
if environ.get('QUERY_STRING'):
|
||
|
url += '?' + environ['QUERY_STRING']
|
||
|
exc = httpexceptions.HTTPMovedPermanently(
|
||
|
'The resource has moved to %s - you should be redirected '
|
||
|
'automatically.' % url,
|
||
|
headers=[('location', url)])
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def find_file(self, environ, base_filename):
|
||
|
possible = []
|
||
|
"""Cache a few values to reduce function call overhead"""
|
||
|
for filename in os.listdir(self.directory):
|
||
|
base, ext = os.path.splitext(filename)
|
||
|
full_filename = os.path.join(self.directory, filename)
|
||
|
if (ext in self.hide_extensions
|
||
|
or not base):
|
||
|
continue
|
||
|
if filename == base_filename:
|
||
|
possible.append(full_filename)
|
||
|
continue
|
||
|
if ext in self.ignore_extensions:
|
||
|
continue
|
||
|
if base == base_filename:
|
||
|
possible.append(full_filename)
|
||
|
if not possible:
|
||
|
#environ['wsgi.errors'].write(
|
||
|
# 'No file found matching %r in %s\n'
|
||
|
# % (base_filename, self.directory))
|
||
|
return None
|
||
|
if len(possible) > 1:
|
||
|
# If there is an exact match, this isn't 'ambiguous'
|
||
|
# per se; it might mean foo.gif and foo.gif.back for
|
||
|
# instance
|
||
|
if full_filename in possible:
|
||
|
return full_filename
|
||
|
else:
|
||
|
environ['wsgi.errors'].write(
|
||
|
'Ambiguous URL: %s; matches files %s\n'
|
||
|
% (request.construct_url(environ),
|
||
|
', '.join(possible)))
|
||
|
return None
|
||
|
return possible[0]
|
||
|
|
||
|
def get_application(self, environ, filename):
|
||
|
if os.path.isdir(filename):
|
||
|
t = 'dir'
|
||
|
else:
|
||
|
t = os.path.splitext(filename)[1]
|
||
|
constructor = self.constructors.get(t, self.constructors.get('*'))
|
||
|
if constructor is None:
|
||
|
#environ['wsgi.errors'].write(
|
||
|
# 'No constructor found for %s\n' % t)
|
||
|
return constructor
|
||
|
app = constructor(self, environ, filename)
|
||
|
if app is None:
|
||
|
#environ['wsgi.errors'].write(
|
||
|
# 'Constructor %s return None for %s\n' %
|
||
|
# (constructor, filename))
|
||
|
pass
|
||
|
return app
|
||
|
|
||
|
def register_constructor(cls, extension, constructor):
|
||
|
"""
|
||
|
Register a function as a constructor. Registered constructors
|
||
|
apply to all instances of `URLParser`.
|
||
|
|
||
|
The extension should have a leading ``.``, or the special
|
||
|
extensions ``dir`` (for directories) and ``*`` (a catch-all).
|
||
|
|
||
|
`constructor` must be a callable that takes two arguments:
|
||
|
``environ`` and ``filename``, and returns a WSGI application.
|
||
|
"""
|
||
|
d = cls.global_constructors
|
||
|
assert extension not in d, (
|
||
|
"A constructor already exists for the extension %r (%r) "
|
||
|
"when attemption to register constructor %r"
|
||
|
% (extension, d[extension], constructor))
|
||
|
d[extension] = constructor
|
||
|
register_constructor = classmethod(register_constructor)
|
||
|
|
||
|
def get_parser(self, directory, base_python_name):
|
||
|
"""
|
||
|
Get a parser for the given directory, or create one if
|
||
|
necessary. This way parsers can be cached and reused.
|
||
|
|
||
|
# @@: settings are inherited from the first caller
|
||
|
"""
|
||
|
try:
|
||
|
return self.parsers_by_directory[(directory, base_python_name)]
|
||
|
except KeyError:
|
||
|
parser = self.__class__(
|
||
|
{},
|
||
|
directory, base_python_name,
|
||
|
index_names=self.index_names,
|
||
|
hide_extensions=self.hide_extensions,
|
||
|
ignore_extensions=self.ignore_extensions,
|
||
|
constructors=self.constructors)
|
||
|
self.parsers_by_directory[(directory, base_python_name)] = parser
|
||
|
return parser
|
||
|
|
||
|
def find_init_module(self, environ):
|
||
|
filename = os.path.join(self.directory, '__init__.py')
|
||
|
if not os.path.exists(filename):
|
||
|
return None
|
||
|
return load_module(environ, filename)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<%s directory=%r; module=%s at %s>' % (
|
||
|
self.__class__.__name__,
|
||
|
self.directory,
|
||
|
self.base_python_name,
|
||
|
hex(abs(id(self))))
|
||
|
|
||
|
def make_directory(parser, environ, filename):
|
||
|
base_python_name = environ['paste.urlparser.base_python_name']
|
||
|
if base_python_name:
|
||
|
base_python_name += "." + os.path.basename(filename)
|
||
|
else:
|
||
|
base_python_name = os.path.basename(filename)
|
||
|
return parser.get_parser(filename, base_python_name)
|
||
|
|
||
|
URLParser.register_constructor('dir', make_directory)
|
||
|
|
||
|
def make_unknown(parser, environ, filename):
|
||
|
return fileapp.FileApp(filename)
|
||
|
|
||
|
URLParser.register_constructor('*', make_unknown)
|
||
|
|
||
|
def load_module(environ, filename):
|
||
|
base_python_name = environ['paste.urlparser.base_python_name']
|
||
|
module_name = os.path.splitext(os.path.basename(filename))[0]
|
||
|
if base_python_name:
|
||
|
module_name = base_python_name + '.' + module_name
|
||
|
return load_module_from_name(environ, filename, module_name,
|
||
|
environ['wsgi.errors'])
|
||
|
|
||
|
def load_module_from_name(environ, filename, module_name, errors):
|
||
|
if module_name in sys.modules:
|
||
|
return sys.modules[module_name]
|
||
|
init_filename = os.path.join(os.path.dirname(filename), '__init__.py')
|
||
|
if not os.path.exists(init_filename):
|
||
|
try:
|
||
|
f = open(init_filename, 'w')
|
||
|
except (OSError, IOError) as e:
|
||
|
errors.write(
|
||
|
'Cannot write __init__.py file into directory %s (%s)\n'
|
||
|
% (os.path.dirname(filename), e))
|
||
|
return None
|
||
|
f.write('#\n')
|
||
|
f.close()
|
||
|
if module_name in sys.modules:
|
||
|
return sys.modules[module_name]
|
||
|
if '.' in module_name:
|
||
|
parent_name = '.'.join(module_name.split('.')[:-1])
|
||
|
base_name = module_name.split('.')[-1]
|
||
|
parent = load_module_from_name(environ, os.path.dirname(filename),
|
||
|
parent_name, errors)
|
||
|
else:
|
||
|
base_name = module_name
|
||
|
module = None
|
||
|
|
||
|
if six.PY2:
|
||
|
fp = None
|
||
|
try:
|
||
|
fp, pathname, stuff = imp.find_module(
|
||
|
base_name, [os.path.dirname(filename)])
|
||
|
module = imp.load_module(module_name, fp, pathname, stuff)
|
||
|
finally:
|
||
|
if fp is not None:
|
||
|
fp.close()
|
||
|
else:
|
||
|
# imp is deprecated and will be removed in Python 3.12
|
||
|
spec = imputil.spec_from_file_location(base_name, filename)
|
||
|
if spec is not None:
|
||
|
module = imputil.module_from_spec(spec)
|
||
|
sys.modules[base_name] = module
|
||
|
spec.loader.exec_module(module)
|
||
|
|
||
|
return module
|
||
|
|
||
|
def make_py(parser, environ, filename):
|
||
|
module = load_module(environ, filename)
|
||
|
if not module:
|
||
|
return None
|
||
|
if hasattr(module, 'application') and module.application:
|
||
|
return getattr(module.application, 'wsgi_application', module.application)
|
||
|
base_name = module.__name__.split('.')[-1]
|
||
|
if hasattr(module, base_name):
|
||
|
obj = getattr(module, base_name)
|
||
|
if hasattr(obj, 'wsgi_application'):
|
||
|
return obj.wsgi_application
|
||
|
else:
|
||
|
# @@: Old behavior; should probably be deprecated eventually:
|
||
|
return getattr(module, base_name)()
|
||
|
environ['wsgi.errors'].write(
|
||
|
"Cound not find application or %s in %s\n"
|
||
|
% (base_name, module))
|
||
|
return None
|
||
|
|
||
|
URLParser.register_constructor('.py', make_py)
|
||
|
|
||
|
class StaticURLParser(object):
|
||
|
"""
|
||
|
Like ``URLParser`` but only serves static files.
|
||
|
|
||
|
``cache_max_age``:
|
||
|
integer specifies Cache-Control max_age in seconds
|
||
|
"""
|
||
|
# @@: Should URLParser subclass from this?
|
||
|
|
||
|
def __init__(self, directory, root_directory=None,
|
||
|
cache_max_age=None):
|
||
|
self.directory = self.normpath(directory)
|
||
|
self.root_directory = self.normpath(root_directory or directory)
|
||
|
self.cache_max_age = cache_max_age
|
||
|
|
||
|
def normpath(path):
|
||
|
return os.path.normcase(os.path.abspath(path))
|
||
|
normpath = staticmethod(normpath)
|
||
|
|
||
|
def __call__(self, environ, start_response):
|
||
|
path_info = environ.get('PATH_INFO', '')
|
||
|
if not path_info:
|
||
|
return self.add_slash(environ, start_response)
|
||
|
if path_info == '/':
|
||
|
# @@: This should obviously be configurable
|
||
|
filename = 'index.html'
|
||
|
else:
|
||
|
filename = request.path_info_pop(environ)
|
||
|
full = self.normpath(os.path.join(self.directory, filename))
|
||
|
if not full.startswith(self.root_directory):
|
||
|
# Out of bounds
|
||
|
return self.not_found(environ, start_response)
|
||
|
if not os.path.exists(full):
|
||
|
return self.not_found(environ, start_response)
|
||
|
if os.path.isdir(full):
|
||
|
# @@: Cache?
|
||
|
return self.__class__(full, root_directory=self.root_directory,
|
||
|
cache_max_age=self.cache_max_age)(environ,
|
||
|
start_response)
|
||
|
if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
|
||
|
return self.error_extra_path(environ, start_response)
|
||
|
if_none_match = environ.get('HTTP_IF_NONE_MATCH')
|
||
|
if if_none_match:
|
||
|
mytime = os.stat(full).st_mtime
|
||
|
if str(mytime) == if_none_match:
|
||
|
headers = []
|
||
|
## FIXME: probably should be
|
||
|
## ETAG.update(headers, '"%s"' % mytime)
|
||
|
ETAG.update(headers, mytime)
|
||
|
start_response('304 Not Modified', headers)
|
||
|
return [''] # empty body
|
||
|
|
||
|
fa = self.make_app(full)
|
||
|
if self.cache_max_age:
|
||
|
fa.cache_control(max_age=self.cache_max_age)
|
||
|
return fa(environ, start_response)
|
||
|
|
||
|
def make_app(self, filename):
|
||
|
return fileapp.FileApp(filename)
|
||
|
|
||
|
def add_slash(self, environ, start_response):
|
||
|
"""
|
||
|
This happens when you try to get to a directory
|
||
|
without a trailing /
|
||
|
"""
|
||
|
url = request.construct_url(environ, with_query_string=False)
|
||
|
url += '/'
|
||
|
if environ.get('QUERY_STRING'):
|
||
|
url += '?' + environ['QUERY_STRING']
|
||
|
exc = httpexceptions.HTTPMovedPermanently(
|
||
|
'The resource has moved to %s - you should be redirected '
|
||
|
'automatically.' % url,
|
||
|
headers=[('location', url)])
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def not_found(self, environ, start_response, debug_message=None):
|
||
|
exc = httpexceptions.HTTPNotFound(
|
||
|
'The resource at %s could not be found'
|
||
|
% request.construct_url(environ),
|
||
|
comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s'
|
||
|
% (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
|
||
|
self.directory, debug_message or '(none)'))
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def error_extra_path(self, environ, start_response):
|
||
|
exc = httpexceptions.HTTPNotFound(
|
||
|
'The trailing path %r is not allowed' % environ['PATH_INFO'])
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<%s %r>' % (self.__class__.__name__, self.directory)
|
||
|
|
||
|
def make_static(global_conf, document_root, cache_max_age=None):
|
||
|
"""
|
||
|
Return a WSGI application that serves a directory (configured
|
||
|
with document_root)
|
||
|
|
||
|
cache_max_age - integer specifies CACHE_CONTROL max_age in seconds
|
||
|
"""
|
||
|
if cache_max_age is not None:
|
||
|
cache_max_age = int(cache_max_age)
|
||
|
return StaticURLParser(
|
||
|
document_root, cache_max_age=cache_max_age)
|
||
|
|
||
|
class PkgResourcesParser(StaticURLParser):
|
||
|
|
||
|
def __init__(self, egg_or_spec, resource_name, manager=None, root_resource=None):
|
||
|
if pkg_resources is None:
|
||
|
raise NotImplementedError("This class requires pkg_resources.")
|
||
|
if isinstance(egg_or_spec, (six.binary_type, six.text_type)):
|
||
|
self.egg = pkg_resources.get_distribution(egg_or_spec)
|
||
|
else:
|
||
|
self.egg = egg_or_spec
|
||
|
self.resource_name = resource_name
|
||
|
if manager is None:
|
||
|
manager = pkg_resources.ResourceManager()
|
||
|
self.manager = manager
|
||
|
if root_resource is None:
|
||
|
root_resource = resource_name
|
||
|
self.root_resource = os.path.normpath(root_resource)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '<%s for %s:%r>' % (
|
||
|
self.__class__.__name__,
|
||
|
self.egg.project_name,
|
||
|
self.resource_name)
|
||
|
|
||
|
def __call__(self, environ, start_response):
|
||
|
path_info = environ.get('PATH_INFO', '')
|
||
|
if not path_info:
|
||
|
return self.add_slash(environ, start_response)
|
||
|
if path_info == '/':
|
||
|
# @@: This should obviously be configurable
|
||
|
filename = 'index.html'
|
||
|
else:
|
||
|
filename = request.path_info_pop(environ)
|
||
|
resource = os.path.normcase(os.path.normpath(
|
||
|
self.resource_name + '/' + filename))
|
||
|
if self.root_resource is not None and not resource.startswith(self.root_resource):
|
||
|
# Out of bounds
|
||
|
return self.not_found(environ, start_response)
|
||
|
if not self.egg.has_resource(resource):
|
||
|
return self.not_found(environ, start_response)
|
||
|
if self.egg.resource_isdir(resource):
|
||
|
# @@: Cache?
|
||
|
child_root = self.root_resource is not None and self.root_resource or \
|
||
|
self.resource_name
|
||
|
return self.__class__(self.egg, resource, self.manager,
|
||
|
root_resource=child_root)(environ, start_response)
|
||
|
if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
|
||
|
return self.error_extra_path(environ, start_response)
|
||
|
|
||
|
type, encoding = mimetypes.guess_type(resource)
|
||
|
if not type:
|
||
|
type = 'application/octet-stream'
|
||
|
# @@: I don't know what to do with the encoding.
|
||
|
try:
|
||
|
file = self.egg.get_resource_stream(self.manager, resource)
|
||
|
except (IOError, OSError) as e:
|
||
|
exc = httpexceptions.HTTPForbidden(
|
||
|
'You are not permitted to view this file (%s)' % e)
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
start_response('200 OK',
|
||
|
[('content-type', type)])
|
||
|
return fileapp._FileIter(file)
|
||
|
|
||
|
def not_found(self, environ, start_response, debug_message=None):
|
||
|
exc = httpexceptions.HTTPNotFound(
|
||
|
'The resource at %s could not be found'
|
||
|
% request.construct_url(environ),
|
||
|
comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in egg:%s#%r; debug: %s'
|
||
|
% (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
|
||
|
self.egg, self.resource_name, debug_message or '(none)'))
|
||
|
return exc.wsgi_application(environ, start_response)
|
||
|
|
||
|
def make_pkg_resources(global_conf, egg, resource_name=''):
|
||
|
"""
|
||
|
A static file parser that loads data from an egg using
|
||
|
``pkg_resources``. Takes a configuration value ``egg``, which is
|
||
|
an egg spec, and a base ``resource_name`` (default empty string)
|
||
|
which is the path in the egg that this starts at.
|
||
|
"""
|
||
|
if pkg_resources is None:
|
||
|
raise NotImplementedError("This function requires pkg_resources.")
|
||
|
return PkgResourcesParser(egg, resource_name)
|
||
|
|
||
|
def make_url_parser(global_conf, directory, base_python_name,
|
||
|
index_names=None, hide_extensions=None,
|
||
|
ignore_extensions=None,
|
||
|
**constructor_conf):
|
||
|
"""
|
||
|
Create a URLParser application that looks in ``directory``, which
|
||
|
should be the directory for the Python package named in
|
||
|
``base_python_name``. ``index_names`` are used when viewing the
|
||
|
directory (like ``'index'`` for ``'index.html'``).
|
||
|
``hide_extensions`` are extensions that are not viewable (like
|
||
|
``'.pyc'``) and ``ignore_extensions`` are viewable but only if an
|
||
|
explicit extension is given.
|
||
|
"""
|
||
|
if index_names is None:
|
||
|
index_names = global_conf.get(
|
||
|
'index_names', ('index', 'Index', 'main', 'Main'))
|
||
|
index_names = converters.aslist(index_names)
|
||
|
|
||
|
if hide_extensions is None:
|
||
|
hide_extensions = global_conf.get(
|
||
|
'hide_extensions', ('.pyc', 'bak', 'py~'))
|
||
|
hide_extensions = converters.aslist(hide_extensions)
|
||
|
|
||
|
if ignore_extensions is None:
|
||
|
ignore_extensions = global_conf.get(
|
||
|
'ignore_extensions', ())
|
||
|
ignore_extensions = converters.aslist(ignore_extensions)
|
||
|
# There's no real way to set constructors currently...
|
||
|
|
||
|
return URLParser({}, directory, base_python_name,
|
||
|
index_names=index_names,
|
||
|
hide_extensions=hide_extensions,
|
||
|
ignore_extensions=ignore_extensions,
|
||
|
**constructor_conf)
|