changeset 4610:79480a001506

upgraded werkzeug again to fix serving bug
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 28 Feb 2009 19:25:46 +0100
parents 246ba4eecab2
children c6e4ccbbb1b3
files MoinMoin/support/werkzeug/contrib/viewdecorators.py MoinMoin/support/werkzeug/datastructures.py MoinMoin/support/werkzeug/serving.py MoinMoin/support/werkzeug/utils.py
diffstat 4 files changed, 71 insertions(+), 99 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/support/werkzeug/contrib/viewdecorators.py	Sat Feb 28 00:08:31 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    werkzeug.contrib.viewdecorators
-    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    Convenience decorators for view callables to return common responses.
-
-    For details on HTTP status codes, see `RFC 2616`_.
-
-    .. _RFC 2616: http://tools.ietf.org/html/rfc2616
-
-    :copyright: (c) 2006-2009 Jochen Kupperschmidt
-    :license: BSD, see LICENSE for more details.
-"""
-
-from functools import wraps
-
-try:
-    import simplejson as json
-except ImportError:
-    import json
-
-from werkzeug.wrappers import Response
-
-
-def jsonify(func):
-    """Return data as JSON response.
-
-    Data returned by the decorated callable is transformed to JSON and wrapped
-    in a response with the appropriate MIME type (as defined in `RFC 4627`_).
-
-    .. _RFC 4627: http://tools.ietf.org/html/rfc4627
-    """
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        data = json.dumps(func(*args, **kwargs))
-        return Response(data, mimetype='application/json')
-    return wrapper
-
-def respond_created(func):
-    """Return a ``201 Created`` response.
-
-    The decorated callable is expected to return the URL of the newly created
-    resource.  That URL is then added to the response as ``Location:`` header.
-    """
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        url = func(*args, **kwargs)
-        return Response(status=201, headers=[('Location', url)])
-    return wrapper
-
-def respond_no_content(func):
-    """Send a ``204 No Content`` response."""
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        func(*args, **kwargs)
-        return Response(status=204)
-    return wrapper
--- a/MoinMoin/support/werkzeug/datastructures.py	Sat Feb 28 00:08:31 2009 +0100
+++ b/MoinMoin/support/werkzeug/datastructures.py	Sat Feb 28 19:25:46 2009 +0100
@@ -5,7 +5,7 @@
 
     This module provides mixins and classes with an immutable interface.
 
-    :copyright: Copyright 2009 by the Werkzeug Team, see AUTHORS for more details.
+    :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re
--- a/MoinMoin/support/werkzeug/serving.py	Sat Feb 28 00:08:31 2009 +0100
+++ b/MoinMoin/support/werkzeug/serving.py	Sat Feb 28 19:25:46 2009 +0100
@@ -119,6 +119,9 @@
             try:
                 for data in application_iter:
                     write(data)
+                # make sure the headers are sent
+                if not headers_sent:
+                    write('')
             finally:
                 if hasattr(application_iter, 'close'):
                     application_iter.close()
--- a/MoinMoin/support/werkzeug/utils.py	Sat Feb 28 00:08:31 2009 +0100
+++ b/MoinMoin/support/werkzeug/utils.py	Sat Feb 28 19:25:46 2009 +0100
@@ -16,8 +16,8 @@
 import urllib
 import urlparse
 import posixpath
-from time import asctime, gmtime, time
-from datetime import timedelta
+from time import time
+from datetime import datetime, timedelta
 
 from werkzeug._internal import _patch_wrapper, _decode_unicode, \
      _empty_stream, _iter_modules, _ExtendedCookie, _ExtendedMorsel, \
@@ -116,18 +116,27 @@
     This will then serve the ``shared_files`` folder in the `myapplication`
     Python package.
 
-    The optional `disallow` parameter can be a list of `fnmatch` rules for
-    files that are not accessible from the web.  If `cache` is set to `False`
-    no caching headers are sent.
+    The optional `disallow` parameter can be a list of :func:`~fnmatch.fnmatch`
+    rules for files that are not accessible from the web.  If `cache` is set to
+    `False` no caching headers are sent.
+
+    .. versionchanged:: 0.5
+       The cache timeout is configurable now.
+
+    :param app: the application to wrap.  If you don't want to wrap an
+                application you can pass it :exc:`NotFound`.
+    :param exports: a dict of exported files and folders.
+    :param diallow: a list of :func:`~fnmatch.fnmatch` rules.
+    :param cache: enable or disable caching headers.
+    :Param cache_timeout: the cache timeout in seconds for the headers.
     """
 
-    # TODO: use wsgi.file_wrapper or something, just don't yield everything
-    # at once.  Also consider switching to BaseResponse
-
-    def __init__(self, app, exports, disallow=None, cache=True):
+    def __init__(self, app, exports, disallow=None, cache=True,
+                 cache_timeout=60 * 60 * 12):
         self.app = app
         self.exports = {}
         self.cache = cache
+        self.cache_timeout = cache_timeout
         for key, value in exports.iteritems():
             if isinstance(value, tuple):
                 loader = self.get_package_loader(*value)
@@ -150,25 +159,42 @@
         """
         return True
 
+    def _opener(self, filename):
+        return lambda: (
+            open(filename, 'rb'),
+            datetime.utcfromtimestamp(os.path.getmtime(filename)),
+            int(os.path.getsize(filename))
+        )
+
     def get_file_loader(self, filename):
-        return lambda x: (os.path.basename(filename), \
-                          lambda: open(filename, 'rb'))
+        return lambda x: (os.path.basename(filename), self._opener(filename))
 
     def get_package_loader(self, package, package_path):
-        from pkg_resources import resource_exists, resource_stream
+        from pkg_resources import DefaultProvider, ResourceManager, \
+             get_provider
+        loadtime = datetime.utcnow()
+        provider = get_provider(package)
+        manager = ResourceManager()
+        filesystem_bound = isinstance(provider, DefaultProvider)
         def loader(path):
-            path = posixpath.join(package_path, path)
-            if resource_exists(package, path):
-                return posixpath.basename(path), \
-                       lambda: resource_stream(package, path)
-            return None, None
+            if not provider.has_resource(path):
+                return None, None
+            basename = posixpath.basename(path)
+            if filesystem_bound:
+                return basename, self._opener(
+                    provider.get_resource_filename(manager, path))
+            return basename, lambda: (
+                provider.get_resource_stream(manager, path),
+                loadtime,
+                0
+            )
         return loader
 
     def get_directory_loader(self, directory):
         def loader(path):
             path = os.path.join(directory, path)
             if os.path.isfile(path):
-                return os.path.basename(path), lambda: open(path, 'rb')
+                return os.path.basename(path), self._opener(path)
             return None, None
         return loader
 
@@ -180,45 +206,47 @@
                 cleaned_path = cleaned_path.replace(sep, '/')
         path = '/'.join([''] + [x for x in cleaned_path.split('/')
                                 if x and x != '..'])
-        stream_maker = None
+        file_loader = None
         for search_path, loader in self.exports.iteritems():
             if search_path == path:
-                real_filename, stream_maker = loader(None)
-                if stream_maker is not None:
+                real_filename, file_loader = loader(None)
+                if file_loader is not None:
                     break
             if not search_path.endswith('/'):
                 search_path += '/'
             if path.startswith(search_path):
-                real_filename, stream_maker = loader(path[len(search_path):])
-                if stream_maker is not None:
+                real_filename, file_loader = loader(path[len(search_path):])
+                if file_loader is not None:
                     break
-        if stream_maker is None or not self.is_allowed(real_filename):
+        if file_loader is None or not self.is_allowed(real_filename):
             return self.app(environ, start_response)
         from mimetypes import guess_type
         guessed_type = guess_type(real_filename)
         mime_type = guessed_type[0] or 'text/plain'
-        expiry = asctime(gmtime(time() + 3600))
-        stream = stream_maker()
-        try:
-            data = stream.read()
-        finally:
-            stream.close()
+        f, mtime, file_size = file_loader()
 
-        headers = [('Cache-Control', 'public')]
+        headers = [('Last-Modified', http_date(mtime)), ('Date', http_date())]
         if self.cache:
-            etag = generate_etag(data)
-            headers += [('Expires', expiry), ('ETag', etag)]
-            if parse_etags(environ.get('HTTP_IF_NONE_MATCH')).contains(etag):
-                remove_entity_headers(headers)
+            timeout = self.cache_timeout
+            etag = 'wzsdm-%s-%s-%s' % (mtime, file_size, hash(real_filename))
+            headers += [
+                ('Etag', '"%s"' % etag),
+                ('Cache-Control', 'max-age=%d, public' % timeout)
+            ]
+            if not is_resource_modified(environ, etag, last_modified=mtime):
+                f.close()
                 start_response('304 Not Modified', headers)
                 return []
+            headers.append(('Expires', http_date(time() + timeout)))
+        else:
+            headers.append(('Cache-Control', 'public'))
 
         headers.extend((
             ('Content-Type', mime_type),
-            ('Content-Length', str(len(data)))
+            ('Content-Length', str(file_size))
         ))
         start_response('200 OK', headers)
-        return [data]
+        return wrap_file(environ, f)
 
 
 class DispatcherMiddleware(object):
@@ -1512,9 +1540,8 @@
 
 
 # circular dependency fun
-from werkzeug.http import generate_etag, parse_etags, \
-     remove_entity_headers, parse_multipart, parse_options_header, \
-     dump_options_header
+from werkzeug.http import parse_multipart, parse_options_header, \
+     is_resource_modified
 from werkzeug.exceptions import BadRequest, RequestEntityTooLarge
 from werkzeug.datastructures import MultiDict, TypeConversionDict