diff options
author | cce <devnull@localhost> | 2005-12-23 01:23:52 +0000 |
---|---|---|
committer | cce <devnull@localhost> | 2005-12-23 01:23:52 +0000 |
commit | 5ffb1dc28cfeeb5e580168c014fee9f239da3273 (patch) | |
tree | 09a7a95ed3eca0b35cd1da38f64dbf71740efc9c /paste/fileapp.py | |
parent | 9b44a755fa49a5e4bee3a6bbc1e09edb9a1609af (diff) | |
download | paste-5ffb1dc28cfeeb5e580168c014fee9f239da3273.tar.gz |
- made __call__ for an HTTPException be the same as wsgi_application
- added if-modified-since logic to fileapp.py
- added helper to mark file/data as cached in fileapp.py
- added starter regression test for fileapp.py
note: if-modified-since is still slighly broken due to timezone issues
(it isn't consistenly keeping gmtime and localtime)
Diffstat (limited to 'paste/fileapp.py')
-rw-r--r-- | paste/fileapp.py | 232 |
1 files changed, 182 insertions, 50 deletions
diff --git a/paste/fileapp.py b/paste/fileapp.py index a6ffe31..4d2d860 100644 --- a/paste/fileapp.py +++ b/paste/fileapp.py @@ -1,103 +1,234 @@ +# (c) 2005 Ian Bicking, Clark C. Evans and contributors +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php """ -Static file sending application +This module handles sending static content such as in-memory data or +files. At this time it has cache helpers and understands the +if-modified-since request header. """ +#@@: this still needs Range support for large files import os, time import mimetypes import httpexceptions -from response import has_header, remove_header -from rfc822 import formatdate +from response import has_header, replace_header +from rfc822 import formatdate, parsedate_tz, mktime_tz +from httpexceptions import HTTPBadRequest CACHE_SIZE = 4096 BLOCK_SIZE = 4096 +__all__ = ['DataApp','FileApp'] + class DataApp(object): """ - Returns an application that will send the data provided. + Returns an application that will send content in a single chunk, + this application has support for setting cashe-control and for + responding to conditional (or HEAD) requests. Constructor Arguments: ``content`` the content being sent to the client - ``headers`` set of static headers to send /w response - - may contain ``content-type`` override - - must not contain ``content-length`` + ``headers`` the headers to send /w the response + + The remaining ``kwargs`` correspond to headers, where the + underscore is replaced with a dash. These values are only + added to the headers if they are not already provided; thus, + they can be used for default values. Examples include, but + are not limited to: + + ``content_type`` + ``content_encoding`` + ``content_location`` + + ``cache()`` + + This method provides validated construction of the ``Cache-Control`` + header as well as providing for automated filling out of the + ``Expires`` header for HTTP/1.0 clients. - ``mimetype`` if set, this is the mimetype of the content + ``set_content()`` - ``expires`` if this is set, is the number of seconds - from the time of the request that the file - is marked to expire + This method provides a mechanism to set the content after the + application has been constructed. This method does things + like changing ``Last-Modified`` and ``Content-Length`` headers. """ - def __init__(self, content, headers=None, expires=None, mimetype=None): + def __init__(self, content, headers=None, **kwargs): + assert isinstance(headers,(type(None),list)) + self.expires = None self.content = None + self.last_modified = 0 self.headers = headers or [] - self.expires = expires + for (k,v) in kwargs.items(): + hk = k.replace("_","-") + if not headers or not has_header(self.headers,hk): + self.headers.append((hk,v)) if not has_header(self.headers,'content-type'): - if not mimetype: - mimetype = 'application/octet-stream' - self.headers.append(('content-type', mimetype)) + self.headers.append(('content-type','application/octet-stream')) if content: self.set_content(content) + def cache(self, public=None, private=None, no_cache=None, + no_store=False, max_age=None, s_maxage=None, + no_transform=False, **extensions): + """ + Sets the ``Cache-Control`` according to the arguments provided. + See RFC 2616 section 14.9 for more details. + + ``public`` if True, this argument specifies that the + response, as a whole, may be cashed. + + ``private`` if True, this argument specifies that the + response, as a whole, may be cashed; this + implementation does not support the + enumeration of private fields + + ``no_cache`` if True, this argument specifies that the + response, as a whole, may be cashed; this + implementation does not support the + enumeration of private fields + + In general, only one of the above three may be True, the other + 2 must then be False or None. If all three are None, then the + cashe is assumed to be ``public``. These are distinct fields + since support for field enumeration may be added in the future. + + ``no_store`` indicates if content may be stored on disk; + otherwise cashe is limited to memory (note: + users can still save the data, this applies + to intermediate caches) + + ``max_age`` the maximum duration (in seconds) for which + the content should be cached; if ``no-cache`` + is specified, this defaults to 0 seconds + + ``s_maxage`` the maximum duration (in seconds) for which the + content should be allowed in a shared cache. + + ``no_transform`` specifies that an intermediate cache should + not convert the content from one type to + another (e.g. transform a BMP to a PNG). + + ``extensions`` gives additional cache-control extensionsn, + such as items like, community="UCI" (14.9.6) + + As recommended by RFC 2616, if ``max_age`` is provided (or + implicitly set by specifying ``no-cache``, then the ``Expires`` + header is also calculated for HTTP/1.0 clients. This is done + """ + assert not has_header(self.headers,'cache-control') + assert not has_header(self.headers,'expires') + assert isinstance(max_age,(type(None),int)) + assert isinstance(s_maxage,(type(None),int)) + result = [] + if private is True: + assert not public and not no_cache and not s_maxage + self.expires = 0 # Date >= Expires for HTTP/1.0 + result.append('private') + elif no_cache is True: + assert not public and not private and not max_age + self.expires = 0 # Date >= Expires for HTTP/1.0 + result.append('no-cache') + else: + assert public is None or public is True + assert not private and not no_cache + self.expires = max_age + result.append('public') + if no_store: + result.append('no-store') + if no_transform: + result.append('no-transform') + if max_age is not None: + result.append('max-age=%d' % max_age) + if s_maxage is not None: + result.append('s-maxage=%d' % s_maxage) + for (k,v) in extensions.items(): + assert '"' not in v + result.append('%s="%s"' % (k.replace("_","-"),v)) + self.headers.append(('cache-control',", ".join(result))) + def set_content(self, content): + self.last_modified = time.time() self.content = [content] - remove_header(self.headers,'content-length') - self.headers.append(('content-length',str(len(content)))) + replace_header(self.headers,'content-length', str(len(content))) + replace_header(self.headers,'last-modified', + formatdate(self.last_modified)) def __call__(self, environ, start_response): - headers = self.headers - if self.expires: - headers = headers[:] # copy this array so we can add - headers.append(('Expires',formatdate(time.time()+self.expires))) - start_response('200 OK',headers) + if self.expires is not None: + replace_header(self.headers,'expires', + formatdate(time.time()+self.expires)) + + checkmod = environ.get('HTTP_IF_MODIFIED_SINCE') + if checkmod: + try: + client_clock = mktime_tz(parsedate_tz(checkmod)) + except TypeError: + return HTTPBadRequest( + "Bad Timestamp\n" + "Client program did not provide an appropriate " + "timestamp for its If-Modified-Since header." + ).wsgi_application(environ, start_response) + if client_clock > time.time(): + return HTTPBadRequest(( + "Clock Time In Future\n" + "According to this server, the time provided in " + "the If-Modified-Since header (%s) is in the future.\n" + "Please check your system clock.") % checkmod + ).wsgi_application(environ, start_response) + elif client_clock <= self.last_modified: + # the client has a recent copy + start_response('304 Not Modified',[]) + return [''] # empty body + + start_response('200 OK',self.headers) return self.content class FileApp(DataApp): """ Returns an application that will send the file at the given filename. Adds a mime type based on ``mimetypes.guess_type()``. + See DataApp for the arguments beyond ``filename``. """ - # @@: Should test things like last-modified, if-modified-since, - # etc. - def __init__(self, filename, **kwargs): + def __init__(self, filename, headers=None, **kwargs): self.filename = filename - self.st_mtime = 0 - if 'mimetype' not in kwargs: - mimetype, encoding = mimetypes.guess_type(self.filename) - # @@: I don't know what to do with the encoding. - if not mimetype: - mimetype = 'application/octet-stream' - kwargs['mimetype'] = mimetype - DataApp.__init__(self, None, **kwargs) + content_type, content_encoding = mimetypes.guess_type(self.filename) + if content_type and 'content_type' not in kwargs: + kwargs['content_type'] = content_type + if content_encoding and 'content_encoding' not in kwargs: + kwargs['content_encoding'] = content_encoding + DataApp.__init__(self, None, headers, **kwargs) def update(self): stat = os.stat(self.filename) - if stat.st_mtime == self.st_mtime: + if stat.st_mtime == self.last_modified: return - self.st_mtime = stat.st_mtime if stat.st_size < CACHE_SIZE: fh = open(self.filename,"rb") self.set_content(fh.read()) fh.close() - return - self.content = None - remove_header(self.headers,'content-length') - self.headers.append(('content-length',stat.st_size)) + else: + self.content = None + replace_header(self.headers, 'content-length', + str(stat.st_size)) + self.last_modified = stat.st_mtime def __call__(self, environ, start_response): self.update() - if self.content: - return DataApp.__call__(self, environ, start_response) - try: - file = open(self.filename, 'rb') - except (IOError, OSError), e: - exc = httpexceptions.HTTPForbidden( - 'You are not permitted to view this file (%s)' % e) - return exc.wsgi_application( - environ, start_response) - DataApp.__call__(self, environ, start_response) + if not self.content: + try: + file = open(self.filename, 'rb') + except (IOError, OSError), e: + exc = httpexceptions.HTTPForbidden( + 'You are not permitted to view this file (%s)' % e) + return exc.wsgi_application( + environ, start_response) + retval = DataApp.__call__(self, environ, start_response) + if retval is not None: + # cached content, exception, or not-modified + return retval return _FileIter(file) class _FileIter: @@ -117,3 +248,4 @@ class _FileIter: def close(self): self.file.close() + |