From 86cf1f8648e3459d78321eedaddfafc1e763ad63 Mon Sep 17 00:00:00 2001 From: Jonathan LaCour Date: Wed, 14 Mar 2012 13:12:18 -0700 Subject: [PATCH] Replacing the Paste static file serving middleware and cascade middleware with a heavily-simplified implementation from Werkzeug, which was thankfully BSD-licensed. Includes a full suite of tests, providing 100% coverage. --- pecan/__init__.py | 8 +- pecan/static.py | 161 ++++++++++++++++++++++++++++++++++++ pecan/tests/static/self.png | Bin 0 -> 6976 bytes pecan/tests/test_static.py | 65 +++++++++++++++ 4 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 pecan/static.py create mode 100644 pecan/tests/static/self.png create mode 100644 pecan/tests/test_static.py diff --git a/pecan/__init__.py b/pecan/__init__.py index 2828e1b..c43b1c0 100644 --- a/pecan/__init__.py +++ b/pecan/__init__.py @@ -1,8 +1,6 @@ -from paste.cascade import Cascade from paste.errordocument import make_errordocument from paste.recursive import RecursiveMiddleware from paste.translogger import TransLogger -from paste.urlparser import StaticURLParser from weberror.errormiddleware import ErrorMiddleware from weberror.evalexception import EvalException @@ -13,10 +11,14 @@ from core import ( from decorators import expose from hooks import RequestViewerHook from templating import error_formatters +from static import SharedDataMiddleware from configuration import set_config from configuration import _runtime_conf as conf +import os + + __all__ = [ 'make_app', 'load_app', 'Pecan', 'request', 'response', 'override_template', 'expose', 'conf', 'set_config', 'render', @@ -54,7 +56,7 @@ def make_app(root, static_root=None, debug=False, errorcfg={}, app = make_errordocument(app, conf, **dict(conf.app.errors)) # Support for serving static files (for development convenience) if static_root: - app = Cascade([StaticURLParser(static_root), app]) + app = SharedDataMiddleware(app, static_root) # Support for simple Apache-style logs if isinstance(logging, dict) or logging == True: app = TransLogger(app, **(isinstance(logging, dict) and logging or {})) diff --git a/pecan/static.py b/pecan/static.py new file mode 100644 index 0000000..78b58c6 --- /dev/null +++ b/pecan/static.py @@ -0,0 +1,161 @@ +""" +This code is adapted from the Werkzeug project, under the BSD license. + +:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + +import os +import mimetypes +from datetime import datetime +from time import gmtime + + +class FileWrapper(object): + """This class can be used to convert a :class:`file`-like object into + an iterable. It yields `buffer_size` blocks until the file is fully + read. + + You should not use this class directly but rather use the + :func:`wrap_file` function that uses the WSGI server's file wrapper + support if it's available. + + :param file: a :class:`file`-like object with a :meth:`~file.read` method. + :param buffer_size: number of bytes for one iteration. + """ + + def __init__(self, file, buffer_size=8192): + self.file = file + self.buffer_size = buffer_size + + def close(self): + if hasattr(self.file, 'close'): + self.file.close() + + def __iter__(self): + return self + + def next(self): + data = self.file.read(self.buffer_size) + if data: + return data + raise StopIteration() + + +def wrap_file(environ, file, buffer_size=8192): + """Wraps a file. This uses the WSGI server's file wrapper if available + or otherwise the generic :class:`FileWrapper`. + + If the file wrapper from the WSGI server is used it's important to not + iterate over it from inside the application but to pass it through + unchanged. + + More information about file wrappers are available in :pep:`333`. + + :param file: a :class:`file`-like object with a :meth:`~file.read` method. + :param buffer_size: number of bytes for one iteration. + """ + return environ.get('wsgi.file_wrapper', FileWrapper)(file, buffer_size) + + +def _dump_date(d, delim): + """Used for `http_date` and `cookie_date`.""" + if d is None: + d = gmtime() + elif isinstance(d, datetime): + d = d.utctimetuple() + elif isinstance(d, (int, long, float)): + d = gmtime(d) + return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % ( + ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[d.tm_wday], + d.tm_mday, delim, + ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec')[d.tm_mon - 1], + delim, str(d.tm_year), d.tm_hour, d.tm_min, d.tm_sec + ) + + +def http_date(timestamp=None): + """Formats the time to match the RFC1123 date format. + + Accepts a floating point number expressed in seconds since the epoch in, a + datetime object or a timetuple. All times in UTC. + + Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``. + + :param timestamp: If provided that date is used, otherwise the current. + """ + return _dump_date(timestamp, ' ') + + +class SharedDataMiddleware(object): + """A WSGI middleware that provides static content for development + environments. + + Currently the middleware does not support non ASCII filenames. If the + encoding on the file system happens to be the encoding of the URI it may + work but this could also be by accident. We strongly suggest using ASCII + only file names for static files. + + The middleware will guess the mimetype using the Python `mimetype` + module. If it's unable to figure out the charset it will fall back + to `fallback_mimetype`. + + :param app: the application to wrap. If you don't want to wrap an + application you can pass it :exc:`NotFound`. + :param directory: the directory to serve up. + :param fallback_mimetype: the fallback mimetype for unknown files. + """ + + def __init__(self, app, directory, fallback_mimetype='text/plain'): + self.app = app + self.directory = directory + self.loader = self.get_directory_loader(directory) + self.fallback_mimetype = fallback_mimetype + + def _opener(self, filename): + return lambda: ( + open(filename, 'rb'), + datetime.utcfromtimestamp(os.path.getmtime(filename)), + int(os.path.getsize(filename)) + ) + + def get_directory_loader(self, directory): + def loader(path): + path = path or directory + if path is not None: + path = os.path.join(directory, path) + if os.path.isfile(path): + return os.path.basename(path), self._opener(path) + return None, None + return loader + + def __call__(self, environ, start_response): + # sanitize the path for non unix systems + cleaned_path = environ.get('PATH_INFO', '').strip('/') + for sep in os.sep, os.altsep: + if sep and sep != '/': + cleaned_path = cleaned_path.replace(sep, '/') + path = '/'.join([''] + [x for x in cleaned_path.split('/') + if x and x != '..']) + + # attempt to find a loader for the file + real_filename, file_loader = self.loader(path[1:]) + if file_loader is None: + return self.app(environ, start_response) + + # serve the file with the appropriate name if we found it + guessed_type = mimetypes.guess_type(real_filename) + mime_type = guessed_type[0] or self.fallback_mimetype + f, mtime, file_size = file_loader() + + headers = [('Date', http_date())] + headers.append(('Cache-Control', 'public')) + headers.extend(( + ('Content-Type', mime_type), + ('Content-Length', str(file_size)), + ('Last-Modified', http_date(mtime)) + )) + + start_response('200 OK', headers) + return wrap_file(environ, f) diff --git a/pecan/tests/static/self.png b/pecan/tests/static/self.png new file mode 100644 index 0000000000000000000000000000000000000000..9b30321f83d46be5fb9c47f5aab0c13ee7a12544 GIT binary patch literal 6976 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 S>tq`M0000