| 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
|---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
|---|
| 3 | """ |
|---|
| 4 | WSGI applications that parse the URL and dispatch to on-disk resources |
|---|
| 5 | """ |
|---|
| 6 | |
|---|
| 7 | import os |
|---|
| 8 | import sys |
|---|
| 9 | import imp |
|---|
| 10 | import mimetypes |
|---|
| 11 | try: |
|---|
| 12 | import pkg_resources |
|---|
| 13 | except ImportError: |
|---|
| 14 | pkg_resources = None |
|---|
| 15 | from paste import request |
|---|
| 16 | from paste import fileapp |
|---|
| 17 | from paste.util import import_string |
|---|
| 18 | from paste import httpexceptions |
|---|
| 19 | from httpheaders import ETAG |
|---|
| 20 | from paste.util import converters |
|---|
| 21 | |
|---|
| 22 | class NoDefault(object): |
|---|
| 23 | pass |
|---|
| 24 | |
|---|
| 25 | __all__ = ['URLParser', 'StaticURLParser', 'PkgResourcesParser'] |
|---|
| 26 | |
|---|
| 27 | class URLParser(object): |
|---|
| 28 | |
|---|
| 29 | """ |
|---|
| 30 | WSGI middleware |
|---|
| 31 | |
|---|
| 32 | Application dispatching, based on URL. An instance of `URLParser` is |
|---|
| 33 | an application that loads and delegates to other applications. It |
|---|
| 34 | looks for files in its directory that match the first part of |
|---|
| 35 | PATH_INFO; these may have an extension, but are not required to have |
|---|
| 36 | one, in which case the available files are searched to find the |
|---|
| 37 | appropriate file. If it is ambiguous, a 404 is returned and an error |
|---|
| 38 | logged. |
|---|
| 39 | |
|---|
| 40 | By default there is a constructor for .py files that loads the module, |
|---|
| 41 | and looks for an attribute ``application``, which is a ready |
|---|
| 42 | application object, or an attribute that matches the module name, |
|---|
| 43 | which is a factory for building applications, and is called with no |
|---|
| 44 | arguments. |
|---|
| 45 | |
|---|
| 46 | URLParser will also look in __init__.py for special overrides. |
|---|
| 47 | These overrides are: |
|---|
| 48 | |
|---|
| 49 | ``urlparser_hook(environ)`` |
|---|
| 50 | This can modify the environment. Its return value is ignored, |
|---|
| 51 | and it cannot be used to change the response in any way. You |
|---|
| 52 | *can* use this, for example, to manipulate SCRIPT_NAME/PATH_INFO |
|---|
| 53 | (try to keep them consistent with the original URL -- but |
|---|
| 54 | consuming PATH_INFO and moving that to SCRIPT_NAME is ok). |
|---|
| 55 | |
|---|
| 56 | ``urlparser_wrap(environ, start_response, app)``: |
|---|
| 57 | After URLParser finds the application, it calls this function |
|---|
| 58 | (if present). If this function doesn't call |
|---|
| 59 | ``app(environ, start_response)`` then the application won't be |
|---|
| 60 | called at all! This can be used to allocate resources (with |
|---|
| 61 | ``try:finally:``) or otherwise filter the output of the |
|---|
| 62 | application. |
|---|
| 63 | |
|---|
| 64 | ``not_found_hook(environ, start_response)``: |
|---|
| 65 | If no file can be found (*in this directory*) to match the |
|---|
| 66 | request, then this WSGI application will be called. You can |
|---|
| 67 | use this to change the URL and pass the request back to |
|---|
| 68 | URLParser again, or on to some other application. This |
|---|
| 69 | doesn't catch all ``404 Not Found`` responses, just missing |
|---|
| 70 | files. |
|---|
| 71 | |
|---|
| 72 | ``application(environ, start_response)``: |
|---|
| 73 | This basically overrides URLParser completely, and the given |
|---|
| 74 | application is used for all requests. ``urlparser_wrap`` and |
|---|
| 75 | ``urlparser_hook`` are still called, but the filesystem isn't |
|---|
| 76 | searched in any way. |
|---|
| 77 | """ |
|---|
| 78 | |
|---|
| 79 | parsers_by_directory = {} |
|---|
| 80 | |
|---|
| 81 | # This is lazily initialized |
|---|
| 82 | init_module = NoDefault |
|---|
| 83 | |
|---|
| 84 | global_constructors = {} |
|---|
| 85 | |
|---|
| 86 | def __init__(self, global_conf, |
|---|
| 87 | directory, base_python_name, |
|---|
| 88 | index_names=NoDefault, |
|---|
| 89 | hide_extensions=NoDefault, |
|---|
| 90 | ignore_extensions=NoDefault, |
|---|
| 91 | constructors=None, |
|---|
| 92 | **constructor_conf): |
|---|
| 93 | """ |
|---|
| 94 | Create a URLParser object that looks at `directory`. |
|---|
| 95 | `base_python_name` is the package that this directory |
|---|
| 96 | represents, thus any Python modules in this directory will |
|---|
| 97 | be given names under this package. |
|---|
| 98 | """ |
|---|
| 99 | if global_conf: |
|---|
| 100 | import warnings |
|---|
| 101 | warnings.warn( |
|---|
| 102 | 'The global_conf argument to URLParser is deprecated; ' |
|---|
| 103 | 'either pass in None or {}, or use make_url_parser', |
|---|
| 104 | DeprecationWarning) |
|---|
| 105 | else: |
|---|
| 106 | global_conf = {} |
|---|
| 107 | if os.path.sep != '/': |
|---|
| 108 | directory = directory.replace(os.path.sep, '/') |
|---|
| 109 | self.directory = directory |
|---|
| 110 | self.base_python_name = base_python_name |
|---|
| 111 | # This logic here should be deprecated since it is in |
|---|
| 112 | # make_url_parser |
|---|
| 113 | if index_names is NoDefault: |
|---|
| 114 | index_names = global_conf.get( |
|---|
| 115 | 'index_names', ('index', 'Index', 'main', 'Main')) |
|---|
| 116 | self.index_names = converters.aslist(index_names) |
|---|
| 117 | if hide_extensions is NoDefault: |
|---|
| 118 | hide_extensions = global_conf.get( |
|---|
| 119 | 'hide_extensions', ('.pyc', '.bak', '.py~', '.pyo')) |
|---|
| 120 | self.hide_extensions = converters.aslist(hide_extensions) |
|---|
| 121 | if ignore_extensions is NoDefault: |
|---|
| 122 | ignore_extensions = global_conf.get( |
|---|
| 123 | 'ignore_extensions', ()) |
|---|
| 124 | self.ignore_extensions = converters.aslist(ignore_extensions) |
|---|
| 125 | self.constructors = self.global_constructors.copy() |
|---|
| 126 | if constructors: |
|---|
| 127 | self.constructors.update(constructors) |
|---|
| 128 | # @@: Should we also check the global options for constructors? |
|---|
| 129 | for name, value in constructor_conf.items(): |
|---|
| 130 | if not name.startswith('constructor '): |
|---|
| 131 | raise ValueError( |
|---|
| 132 | "Only extra configuration keys allowed are " |
|---|
| 133 | "'constructor .ext = import_expr'; you gave %r " |
|---|
| 134 | "(=%r)" % (name, value)) |
|---|
| 135 | ext = name[len('constructor '):].strip() |
|---|
| 136 | if isinstance(value, (str, unicode)): |
|---|
| 137 | value = import_string.eval_import(value) |
|---|
| 138 | self.constructors[ext] = value |
|---|
| 139 | |
|---|
| 140 | def __call__(self, environ, start_response): |
|---|
| 141 | environ['paste.urlparser.base_python_name'] = self.base_python_name |
|---|
| 142 | if self.init_module is NoDefault: |
|---|
| 143 | self.init_module = self.find_init_module(environ) |
|---|
| 144 | path_info = environ.get('PATH_INFO', '') |
|---|
| 145 | if not path_info: |
|---|
| 146 | return self.add_slash(environ, start_response) |
|---|
| 147 | if (self.init_module |
|---|
| 148 | and getattr(self.init_module, 'urlparser_hook', None)): |
|---|
| 149 | self.init_module.urlparser_hook(environ) |
|---|
| 150 | orig_path_info = environ['PATH_INFO'] |
|---|
| 151 | orig_script_name = environ['SCRIPT_NAME'] |
|---|
| 152 | application, filename = self.find_application(environ) |
|---|
| 153 | if not application: |
|---|
| 154 | if (self.init_module |
|---|
| 155 | and getattr(self.init_module, 'not_found_hook', None) |
|---|
| 156 | and environ.get('paste.urlparser.not_found_parser') is not self): |
|---|
| 157 | not_found_hook = self.init_module.not_found_hook |
|---|
| 158 | environ['paste.urlparser.not_found_parser'] = self |
|---|
| 159 | environ['PATH_INFO'] = orig_path_info |
|---|
| 160 | environ['SCRIPT_NAME'] = orig_script_name |
|---|
| 161 | return not_found_hook(environ, start_response) |
|---|
| 162 | if filename is None: |
|---|
| 163 | name, rest_of_path = request.path_info_split(environ['PATH_INFO']) |
|---|
| 164 | if not name: |
|---|
| 165 | name = 'one of %s' % ', '.join( |
|---|
| 166 | self.index_names or |
|---|
| 167 | ['(no index_names defined)']) |
|---|
| 168 | |
|---|
| 169 | return self.not_found( |
|---|
| 170 | environ, start_response, |
|---|
| 171 | 'Tried to load %s from directory %s' |
|---|
| 172 | % (name, self.directory)) |
|---|
| 173 | else: |
|---|
| 174 | environ['wsgi.errors'].write( |
|---|
| 175 | 'Found resource %s, but could not construct application\n' |
|---|
| 176 | % filename) |
|---|
| 177 | return self.not_found( |
|---|
| 178 | environ, start_response, |
|---|
| 179 | 'Tried to load %s from directory %s' |
|---|
| 180 | % (filename, self.directory)) |
|---|
| 181 | if (self.init_module |
|---|
| 182 | and getattr(self.init_module, 'urlparser_wrap', None)): |
|---|
| 183 | return self.init_module.urlparser_wrap( |
|---|
| 184 | environ, start_response, application) |
|---|
| 185 | else: |
|---|
| 186 | return application(environ, start_response) |
|---|
| 187 | |
|---|
| 188 | def find_application(self, environ): |
|---|
| 189 | if (self.init_module |
|---|
| 190 | and getattr(self.init_module, 'application', None) |
|---|
| 191 | and not environ.get('paste.urlparser.init_application') == environ['SCRIPT_NAME']): |
|---|
| 192 | environ['paste.urlparser.init_application'] = environ['SCRIPT_NAME'] |
|---|
| 193 | return self.init_module.application, None |
|---|
| 194 | name, rest_of_path = request.path_info_split(environ['PATH_INFO']) |
|---|
| 195 | environ['PATH_INFO'] = rest_of_path |
|---|
| 196 | if name is not None: |
|---|
| 197 | environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + '/' + name |
|---|
| 198 | if not name: |
|---|
| 199 | names = self.index_names |
|---|
| 200 | for index_name in names: |
|---|
| 201 | filename = self.find_file(environ, index_name) |
|---|
| 202 | if filename: |
|---|
| 203 | break |
|---|
| 204 | else: |
|---|
| 205 | # None of the index files found |
|---|
| 206 | filename = None |
|---|
| 207 | else: |
|---|
| 208 | filename = self.find_file(environ, name) |
|---|
| 209 | if filename is None: |
|---|
| 210 | return None, filename |
|---|
| 211 | else: |
|---|
| 212 | return self.get_application(environ, filename), filename |
|---|
| 213 | |
|---|
| 214 | def not_found(self, environ, start_response, debug_message=None): |
|---|
| 215 | exc = httpexceptions.HTTPNotFound( |
|---|
| 216 | 'The resource at %s could not be found' |
|---|
| 217 | % request.construct_url(environ), |
|---|
| 218 | comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s' |
|---|
| 219 | % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'), |
|---|
| 220 | self.directory, debug_message or '(none)')) |
|---|
| 221 | return exc.wsgi_application(environ, start_response) |
|---|
| 222 | |
|---|
| 223 | def add_slash(self, environ, start_response): |
|---|
| 224 | """ |
|---|
| 225 | This happens when you try to get to a directory |
|---|
| 226 | without a trailing / |
|---|
| 227 | """ |
|---|
| 228 | url = request.construct_url(environ, with_query_string=False) |
|---|
| 229 | url += '/' |
|---|
| 230 | if environ.get('QUERY_STRING'): |
|---|
| 231 | url += '?' + environ['QUERY_STRING'] |
|---|
| 232 | exc = httpexceptions.HTTPMovedPermanently( |
|---|
| 233 | 'The resource has moved to %s - you should be redirected ' |
|---|
| 234 | 'automatically.''' % url, |
|---|
| 235 | headers=[('location', url)]) |
|---|
| 236 | return exc.wsgi_application(environ, start_response) |
|---|
| 237 | |
|---|
| 238 | def find_file(self, environ, base_filename): |
|---|
| 239 | possible = [] |
|---|
| 240 | """Cache a few values to reduce function call overhead""" |
|---|
| 241 | for filename in os.listdir(self.directory): |
|---|
| 242 | base, ext = os.path.splitext(filename) |
|---|
| 243 | full_filename = os.path.join(self.directory, filename) |
|---|
| 244 | if (ext in self.hide_extensions |
|---|
| 245 | or not base): |
|---|
| 246 | continue |
|---|
| 247 | if filename == base_filename: |
|---|
| 248 | possible.append(full_filename) |
|---|
| 249 | continue |
|---|
| 250 | if ext in self.ignore_extensions: |
|---|
| 251 | continue |
|---|
| 252 | if base == base_filename: |
|---|
| 253 | possible.append(full_filename) |
|---|
| 254 | if not possible: |
|---|
| 255 | #environ['wsgi.errors'].write( |
|---|
| 256 | # 'No file found matching %r in %s\n' |
|---|
| 257 | # % (base_filename, self.directory)) |
|---|
| 258 | return None |
|---|
| 259 | if len(possible) > 1: |
|---|
| 260 | # If there is an exact match, this isn't 'ambiguous' |
|---|
| 261 | # per se; it might mean foo.gif and foo.gif.back for |
|---|
| 262 | # instance |
|---|
| 263 | if full_filename in possible: |
|---|
| 264 | return full_filename |
|---|
| 265 | else: |
|---|
| 266 | environ['wsgi.errors'].write( |
|---|
| 267 | 'Ambiguous URL: %s; matches files %s\n' |
|---|
| 268 | % (request.construct_url(environ), |
|---|
| 269 | ', '.join(possible))) |
|---|
| 270 | return None |
|---|
| 271 | return possible[0] |
|---|
| 272 | |
|---|
| 273 | def get_application(self, environ, filename): |
|---|
| 274 | if os.path.isdir(filename): |
|---|
| 275 | t = 'dir' |
|---|
| 276 | else: |
|---|
| 277 | t = os.path.splitext(filename)[1] |
|---|
| 278 | constructor = self.constructors.get(t, self.constructors.get('*')) |
|---|
| 279 | if constructor is None: |
|---|
| 280 | #environ['wsgi.errors'].write( |
|---|
| 281 | # 'No constructor found for %s\n' % t) |
|---|
| 282 | return constructor |
|---|
| 283 | app = constructor(self, environ, filename) |
|---|
| 284 | if app is None: |
|---|
| 285 | #environ['wsgi.errors'].write( |
|---|
| 286 | # 'Constructor %s return None for %s\n' % |
|---|
| 287 | # (constructor, filename)) |
|---|
| 288 | pass |
|---|
| 289 | return app |
|---|
| 290 | |
|---|
| 291 | def register_constructor(cls, extension, constructor): |
|---|
| 292 | """ |
|---|
| 293 | Register a function as a constructor. Registered constructors |
|---|
| 294 | apply to all instances of `URLParser`. |
|---|
| 295 | |
|---|
| 296 | The extension should have a leading ``.``, or the special |
|---|
| 297 | extensions ``dir`` (for directories) and ``*`` (a catch-all). |
|---|
| 298 | |
|---|
| 299 | `constructor` must be a callable that takes two arguments: |
|---|
| 300 | ``environ`` and ``filename``, and returns a WSGI application. |
|---|
| 301 | """ |
|---|
| 302 | d = cls.global_constructors |
|---|
| 303 | assert not d.has_key(extension), ( |
|---|
| 304 | "A constructor already exists for the extension %r (%r) " |
|---|
| 305 | "when attemption to register constructor %r" |
|---|
| 306 | % (extension, d[extension], constructor)) |
|---|
| 307 | d[extension] = constructor |
|---|
| 308 | register_constructor = classmethod(register_constructor) |
|---|
| 309 | |
|---|
| 310 | def get_parser(self, directory, base_python_name): |
|---|
| 311 | """ |
|---|
| 312 | Get a parser for the given directory, or create one if |
|---|
| 313 | necessary. This way parsers can be cached and reused. |
|---|
| 314 | |
|---|
| 315 | # @@: settings are inherited from the first caller |
|---|
| 316 | """ |
|---|
| 317 | try: |
|---|
| 318 | return self.parsers_by_directory[(directory, base_python_name)] |
|---|
| 319 | except KeyError: |
|---|
| 320 | parser = self.__class__( |
|---|
| 321 | {}, |
|---|
| 322 | directory, base_python_name, |
|---|
| 323 | index_names=self.index_names, |
|---|
| 324 | hide_extensions=self.hide_extensions, |
|---|
| 325 | ignore_extensions=self.ignore_extensions, |
|---|
| 326 | constructors=self.constructors) |
|---|
| 327 | self.parsers_by_directory[(directory, base_python_name)] = parser |
|---|
| 328 | return parser |
|---|
| 329 | |
|---|
| 330 | def find_init_module(self, environ): |
|---|
| 331 | filename = os.path.join(self.directory, '__init__.py') |
|---|
| 332 | if not os.path.exists(filename): |
|---|
| 333 | return None |
|---|
| 334 | return load_module(environ, filename) |
|---|
| 335 | |
|---|
| 336 | def __repr__(self): |
|---|
| 337 | return '<%s directory=%r; module=%s at %s>' % ( |
|---|
| 338 | self.__class__.__name__, |
|---|
| 339 | self.directory, |
|---|
| 340 | self.base_python_name, |
|---|
| 341 | hex(abs(id(self)))) |
|---|
| 342 | |
|---|
| 343 | def make_directory(parser, environ, filename): |
|---|
| 344 | base_python_name = environ['paste.urlparser.base_python_name'] |
|---|
| 345 | if base_python_name: |
|---|
| 346 | base_python_name += "." + os.path.basename(filename) |
|---|
| 347 | else: |
|---|
| 348 | base_python_name = os.path.basename(filename) |
|---|
| 349 | return parser.get_parser(filename, base_python_name) |
|---|
| 350 | |
|---|
| 351 | URLParser.register_constructor('dir', make_directory) |
|---|
| 352 | |
|---|
| 353 | def make_unknown(parser, environ, filename): |
|---|
| 354 | return fileapp.FileApp(filename) |
|---|
| 355 | |
|---|
| 356 | URLParser.register_constructor('*', make_unknown) |
|---|
| 357 | |
|---|
| 358 | def load_module(environ, filename): |
|---|
| 359 | base_python_name = environ['paste.urlparser.base_python_name'] |
|---|
| 360 | module_name = os.path.splitext(os.path.basename(filename))[0] |
|---|
| 361 | if base_python_name: |
|---|
| 362 | module_name = base_python_name + '.' + module_name |
|---|
| 363 | return load_module_from_name(environ, filename, module_name, |
|---|
| 364 | environ['wsgi.errors']) |
|---|
| 365 | |
|---|
| 366 | def load_module_from_name(environ, filename, module_name, errors): |
|---|
| 367 | if sys.modules.has_key(module_name): |
|---|
| 368 | return sys.modules[module_name] |
|---|
| 369 | init_filename = os.path.join(os.path.dirname(filename), '__init__.py') |
|---|
| 370 | if not os.path.exists(init_filename): |
|---|
| 371 | try: |
|---|
| 372 | f = open(init_filename, 'w') |
|---|
| 373 | except (OSError, IOError), e: |
|---|
| 374 | errors.write( |
|---|
| 375 | 'Cannot write __init__.py file into directory %s (%s)\n' |
|---|
| 376 | % (os.path.dirname(filename), e)) |
|---|
| 377 | return None |
|---|
| 378 | f.write('#\n') |
|---|
| 379 | f.close() |
|---|
| 380 | fp = None |
|---|
| 381 | if sys.modules.has_key(module_name): |
|---|
| 382 | return sys.modules[module_name] |
|---|
| 383 | if '.' in module_name: |
|---|
| 384 | parent_name = '.'.join(module_name.split('.')[:-1]) |
|---|
| 385 | base_name = module_name.split('.')[-1] |
|---|
| 386 | parent = load_module_from_name(environ, os.path.dirname(filename), |
|---|
| 387 | parent_name, errors) |
|---|
| 388 | else: |
|---|
| 389 | base_name = module_name |
|---|
| 390 | fp = None |
|---|
| 391 | try: |
|---|
| 392 | fp, pathname, stuff = imp.find_module( |
|---|
| 393 | base_name, [os.path.dirname(filename)]) |
|---|
| 394 | module = imp.load_module(module_name, fp, pathname, stuff) |
|---|
| 395 | finally: |
|---|
| 396 | if fp is not None: |
|---|
| 397 | fp.close() |
|---|
| 398 | return module |
|---|
| 399 | |
|---|
| 400 | def make_py(parser, environ, filename): |
|---|
| 401 | module = load_module(environ, filename) |
|---|
| 402 | if not module: |
|---|
| 403 | return None |
|---|
| 404 | if hasattr(module, 'application') and module.application: |
|---|
| 405 | return getattr(module.application, 'wsgi_application', module.application) |
|---|
| 406 | base_name = module.__name__.split('.')[-1] |
|---|
| 407 | if hasattr(module, base_name): |
|---|
| 408 | obj = getattr(module, base_name) |
|---|
| 409 | if hasattr(obj, 'wsgi_application'): |
|---|
| 410 | return obj.wsgi_application |
|---|
| 411 | else: |
|---|
| 412 | # @@: Old behavior; should probably be deprecated eventually: |
|---|
| 413 | return getattr(module, base_name)() |
|---|
| 414 | environ['wsgi.errors'].write( |
|---|
| 415 | "Cound not find application or %s in %s\n" |
|---|
| 416 | % (base_name, module)) |
|---|
| 417 | return None |
|---|
| 418 | |
|---|
| 419 | URLParser.register_constructor('.py', make_py) |
|---|
| 420 | |
|---|
| 421 | class StaticURLParser(object): |
|---|
| 422 | |
|---|
| 423 | """ |
|---|
| 424 | Like ``URLParser`` but only serves static files. |
|---|
| 425 | |
|---|
| 426 | ``cache_max_age``: |
|---|
| 427 | integer specifies Cache-Control max_age in seconds |
|---|
| 428 | """ |
|---|
| 429 | # @@: Should URLParser subclass from this? |
|---|
| 430 | |
|---|
| 431 | def __init__(self, directory, root_directory=None, |
|---|
| 432 | cache_max_age=None): |
|---|
| 433 | if os.path.sep != '/': |
|---|
| 434 | directory = directory.replace(os.path.sep, '/') |
|---|
| 435 | self.directory = directory |
|---|
| 436 | self.root_directory = root_directory |
|---|
| 437 | if root_directory is not None: |
|---|
| 438 | self.root_directory = os.path.normpath(self.root_directory) |
|---|
| 439 | else: |
|---|
| 440 | self.root_directory = directory |
|---|
| 441 | self.cache_max_age = cache_max_age |
|---|
| 442 | if os.path.sep != '/': |
|---|
| 443 | directory = directory.replace('/', os.path.sep) |
|---|
| 444 | self.root_directory = self.root_directory.replace('/', os.path.sep) |
|---|
| 445 | |
|---|
| 446 | def __call__(self, environ, start_response): |
|---|
| 447 | path_info = environ.get('PATH_INFO', '') |
|---|
| 448 | if not path_info: |
|---|
| 449 | return self.add_slash(environ, start_response) |
|---|
| 450 | if path_info == '/': |
|---|
| 451 | # @@: This should obviously be configurable |
|---|
| 452 | filename = 'index.html' |
|---|
| 453 | else: |
|---|
| 454 | filename = request.path_info_pop(environ) |
|---|
| 455 | full = os.path.normpath(os.path.join(self.directory, filename)) |
|---|
| 456 | if os.path.sep != '/': |
|---|
| 457 | full = full.replace('/', os.path.sep) |
|---|
| 458 | if self.root_directory is not None and not full.startswith(self.root_directory): |
|---|
| 459 | # Out of bounds |
|---|
| 460 | return self.not_found(environ, start_response) |
|---|
| 461 | if not os.path.exists(full): |
|---|
| 462 | return self.not_found(environ, start_response) |
|---|
| 463 | if os.path.isdir(full): |
|---|
| 464 | # @@: Cache? |
|---|
| 465 | child_root = self.root_directory is not None and \ |
|---|
| 466 | self.root_directory or self.directory |
|---|
| 467 | return self.__class__(full, root_directory=child_root, |
|---|
| 468 | cache_max_age=self.cache_max_age)(environ, |
|---|
| 469 | start_response) |
|---|
| 470 | if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/': |
|---|
| 471 | return self.error_extra_path(environ, start_response) |
|---|
| 472 | if_none_match = environ.get('HTTP_IF_NONE_MATCH') |
|---|
| 473 | if if_none_match: |
|---|
| 474 | mytime = os.stat(full).st_mtime |
|---|
| 475 | if str(mytime) == if_none_match: |
|---|
| 476 | headers = [] |
|---|
| 477 | ETAG.update(headers, mytime) |
|---|
| 478 | start_response('304 Not Modified', headers) |
|---|
| 479 | return [''] # empty body |
|---|
| 480 | |
|---|
| 481 | fa = self.make_app(full) |
|---|
| 482 | if self.cache_max_age: |
|---|
| 483 | fa.cache_control(max_age=self.cache_max_age) |
|---|
| 484 | return fa(environ, start_response) |
|---|
| 485 | |
|---|
| 486 | def make_app(self, filename): |
|---|
| 487 | return fileapp.FileApp(filename) |
|---|
| 488 | |
|---|
| 489 | def add_slash(self, environ, start_response): |
|---|
| 490 | """ |
|---|
| 491 | This happens when you try to get to a directory |
|---|
| 492 | without a trailing / |
|---|
| 493 | """ |
|---|
| 494 | url = request.construct_url(environ, with_query_string=False) |
|---|
| 495 | url += '/' |
|---|
| 496 | if environ.get('QUERY_STRING'): |
|---|
| 497 | url += '?' + environ['QUERY_STRING'] |
|---|
| 498 | exc = httpexceptions.HTTPMovedPermanently( |
|---|
| 499 | 'The resource has moved to %s - you should be redirected ' |
|---|
| 500 | 'automatically.''' % url, |
|---|
| 501 | headers=[('location', url)]) |
|---|
| 502 | return exc.wsgi_application(environ, start_response) |
|---|
| 503 | |
|---|
| 504 | def not_found(self, environ, start_response, debug_message=None): |
|---|
| 505 | exc = httpexceptions.HTTPNotFound( |
|---|
| 506 | 'The resource at %s could not be found' |
|---|
| 507 | % request.construct_url(environ), |
|---|
| 508 | comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s' |
|---|
| 509 | % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'), |
|---|
| 510 | self.directory, debug_message or '(none)')) |
|---|
| 511 | return exc.wsgi_application(environ, start_response) |
|---|
| 512 | |
|---|
| 513 | def error_extra_path(self, environ, start_response): |
|---|
| 514 | exc = httpexceptions.HTTPNotFound( |
|---|
| 515 | 'The trailing path %r is not allowed' % environ['PATH_INFO']) |
|---|
| 516 | return exc.wsgi_application(environ, start_response) |
|---|
| 517 | |
|---|
| 518 | def __repr__(self): |
|---|
| 519 | return '<%s %r>' % (self.__class__.__name__, self.directory) |
|---|
| 520 | |
|---|
| 521 | def make_static(global_conf, document_root, cache_max_age=None): |
|---|
| 522 | """ |
|---|
| 523 | Return a WSGI application that serves a directory (configured |
|---|
| 524 | with document_root) |
|---|
| 525 | |
|---|
| 526 | cache_max_age - integer specifies CACHE_CONTROL max_age in seconds |
|---|
| 527 | """ |
|---|
| 528 | if cache_max_age is not None: |
|---|
| 529 | cache_max_age = int(cache_max_age) |
|---|
| 530 | return StaticURLParser( |
|---|
| 531 | document_root, cache_max_age=cache_max_age) |
|---|
| 532 | |
|---|
| 533 | class PkgResourcesParser(StaticURLParser): |
|---|
| 534 | |
|---|
| 535 | def __init__(self, egg_or_spec, resource_name, manager=None, root_resource=None): |
|---|
| 536 | if pkg_resources is None: |
|---|
| 537 | raise NotImplementedError("This class requires pkg_resources.") |
|---|
| 538 | if isinstance(egg_or_spec, (str, unicode)): |
|---|
| 539 | self.egg = pkg_resources.get_distribution(egg_or_spec) |
|---|
| 540 | else: |
|---|
| 541 | self.egg = egg_or_spec |
|---|
| 542 | self.resource_name = resource_name |
|---|
| 543 | if manager is None: |
|---|
| 544 | manager = pkg_resources.ResourceManager() |
|---|
| 545 | self.manager = manager |
|---|
| 546 | if root_resource is None: |
|---|
| 547 | root_resource = resource_name |
|---|
| 548 | self.root_resource = os.path.normpath(root_resource) |
|---|
| 549 | |
|---|
| 550 | def __repr__(self): |
|---|
| 551 | return '<%s for %s:%r>' % ( |
|---|
| 552 | self.__class__.__name__, |
|---|
| 553 | self.egg.project_name, |
|---|
| 554 | self.resource_name) |
|---|
| 555 | |
|---|
| 556 | def __call__(self, environ, start_response): |
|---|
| 557 | path_info = environ.get('PATH_INFO', '') |
|---|
| 558 | if not path_info: |
|---|
| 559 | return self.add_slash(environ, start_response) |
|---|
| 560 | if path_info == '/': |
|---|
| 561 | # @@: This should obviously be configurable |
|---|
| 562 | filename = 'index.html' |
|---|
| 563 | else: |
|---|
| 564 | filename = request.path_info_pop(environ) |
|---|
| 565 | resource = os.path.normpath(self.resource_name + '/' + filename) |
|---|
| 566 | if self.root_resource is not None and not resource.startswith(self.root_resource): |
|---|
| 567 | # Out of bounds |
|---|
| 568 | return self.not_found(environ, start_response) |
|---|
| 569 | if not self.egg.has_resource(resource): |
|---|
| 570 | return self.not_found(environ, start_response) |
|---|
| 571 | if self.egg.resource_isdir(resource): |
|---|
| 572 | # @@: Cache? |
|---|
| 573 | child_root = self.root_resource is not None and self.root_resource or \ |
|---|
| 574 | self.resource_name |
|---|
| 575 | return self.__class__(self.egg, resource, self.manager, |
|---|
| 576 | root_resource=child_root)(environ, start_response) |
|---|
| 577 | if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/': |
|---|
| 578 | return self.error_extra_path(environ, start_response) |
|---|
| 579 | |
|---|
| 580 | type, encoding = mimetypes.guess_type(resource) |
|---|
| 581 | if not type: |
|---|
| 582 | type = 'application/octet-stream' |
|---|
| 583 | # @@: I don't know what to do with the encoding. |
|---|
| 584 | try: |
|---|
| 585 | file = self.egg.get_resource_stream(self.manager, resource) |
|---|
| 586 | except (IOError, OSError), e: |
|---|
| 587 | exc = httpexceptions.HTTPForbidden( |
|---|
| 588 | 'You are not permitted to view this file (%s)' % e) |
|---|
| 589 | return exc.wsgi_application(environ, start_response) |
|---|
| 590 | start_response('200 OK', |
|---|
| 591 | [('content-type', type)]) |
|---|
| 592 | return fileapp._FileIter(file) |
|---|
| 593 | |
|---|
| 594 | def not_found(self, environ, start_response, debug_message=None): |
|---|
| 595 | exc = httpexceptions.HTTPNotFound( |
|---|
| 596 | 'The resource at %s could not be found' |
|---|
| 597 | % request.construct_url(environ), |
|---|
| 598 | comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in egg:%s#%r; debug: %s' |
|---|
| 599 | % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'), |
|---|
| 600 | self.egg, self.resource_name, debug_message or '(none)')) |
|---|
| 601 | return exc.wsgi_application(environ, start_response) |
|---|
| 602 | |
|---|
| 603 | def make_pkg_resources(global_conf, egg, resource_name=''): |
|---|
| 604 | """ |
|---|
| 605 | A static file parser that loads data from an egg using |
|---|
| 606 | ``pkg_resources``. Takes a configuration value ``egg``, which is |
|---|
| 607 | an egg spec, and a base ``resource_name`` (default empty string) |
|---|
| 608 | which is the path in the egg that this starts at. |
|---|
| 609 | """ |
|---|
| 610 | if pkg_resources is None: |
|---|
| 611 | raise NotImplementedError("This function requires pkg_resources.") |
|---|
| 612 | return PkgResourcesParser(egg, resource_name) |
|---|
| 613 | |
|---|
| 614 | def make_url_parser(global_conf, directory, base_python_name, |
|---|
| 615 | index_names=None, hide_extensions=None, |
|---|
| 616 | ignore_extensions=None, |
|---|
| 617 | **constructor_conf): |
|---|
| 618 | """ |
|---|
| 619 | Create a URLParser application that looks in ``directory``, which |
|---|
| 620 | should be the directory for the Python package named in |
|---|
| 621 | ``base_python_name``. ``index_names`` are used when viewing the |
|---|
| 622 | directory (like ``'index'`` for ``'index.html'``). |
|---|
| 623 | ``hide_extensions`` are extensions that are not viewable (like |
|---|
| 624 | ``'.pyc'``) and ``ignore_extensions`` are viewable but only if an |
|---|
| 625 | explicit extension is given. |
|---|
| 626 | """ |
|---|
| 627 | if index_names is None: |
|---|
| 628 | index_names = global_conf.get( |
|---|
| 629 | 'index_names', ('index', 'Index', 'main', 'Main')) |
|---|
| 630 | index_names = converters.aslist(index_names) |
|---|
| 631 | |
|---|
| 632 | if hide_extensions is None: |
|---|
| 633 | hide_extensions = global_conf.get( |
|---|
| 634 | 'hide_extensions', ('.pyc', 'bak', 'py~')) |
|---|
| 635 | hide_extensions = converters.aslist(hide_extensions) |
|---|
| 636 | |
|---|
| 637 | if ignore_extensions is None: |
|---|
| 638 | ignore_extensions = global_conf.get( |
|---|
| 639 | 'ignore_extensions', ()) |
|---|
| 640 | ignore_extensions = converters.aslist(ignore_extensions) |
|---|
| 641 | # There's no real way to set constructors currently... |
|---|
| 642 | |
|---|
| 643 | return URLParser({}, directory, base_python_name, |
|---|
| 644 | index_names=index_names, |
|---|
| 645 | hide_extensions=hide_extensions, |
|---|
| 646 | ignore_extensions=ignore_extensions, |
|---|
| 647 | **constructor_conf) |
|---|
| 648 | |
|---|