Home | History | Annotate | Download | only in docs
      1 WebOb File-Serving Example
      2 ==========================
      3 
      4 This document shows how you can make a static-file-serving application
      5 using WebOb.  We'll quickly build this up from minimal functionality
      6 to a high-quality file serving application.
      7 
      8 .. note:: Starting from 1.2b4, WebOb ships with a :mod:`webob.static` module
      9     which implements a :class:`webob.static.FileApp` WSGI application similar to the
     10     one described below.
     11 
     12     This document stays as a didactic example how to serve files with WebOb, but
     13     you should consider using applications from :mod:`webob.static` in
     14     production.
     15 
     16 .. comment:
     17 
     18    >>> import webob, os
     19    >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__))
     20    >>> doc_dir = os.path.join(base_dir, 'docs')
     21    >>> from doctest import ELLIPSIS
     22 
     23 First we'll setup a really simple shim around our application, which
     24 we can use as we improve our application:
     25 
     26 .. code-block:: python
     27 
     28    >>> from webob import Request, Response
     29    >>> import os
     30    >>> class FileApp(object):
     31    ...     def __init__(self, filename):
     32    ...         self.filename = filename
     33    ...     def __call__(self, environ, start_response):
     34    ...         res = make_response(self.filename)
     35    ...         return res(environ, start_response)
     36    >>> import mimetypes
     37    >>> def get_mimetype(filename):
     38    ...     type, encoding = mimetypes.guess_type(filename)
     39    ...     # We'll ignore encoding, even though we shouldn't really
     40    ...     return type or 'application/octet-stream'
     41 
     42 Now we can make different definitions of ``make_response``.  The
     43 simplest version:
     44 
     45 .. code-block:: python
     46 
     47    >>> def make_response(filename):
     48    ...     res = Response(content_type=get_mimetype(filename))
     49    ...     res.body = open(filename, 'rb').read()
     50    ...     return res
     51 
     52 Let's give it a go.  We'll test it out with a file ``test-file.txt``
     53 in the WebOb doc directory:
     54 
     55 .. code-block:: python
     56 
     57    >>> fn = os.path.join(doc_dir, 'test-file.txt')
     58    >>> open(fn).read()
     59    'This is a test.  Hello test people!'
     60    >>> app = FileApp(fn)
     61    >>> req = Request.blank('/')
     62    >>> print req.get_response(app)
     63    200 OK
     64    Content-Type: text/plain; charset=UTF-8
     65    Content-Length: 35
     66    <BLANKLINE>
     67    This is a test.  Hello test people!
     68 
     69 Well, that worked.  But it's not a very fancy object.  First, it reads
     70 everything into memory, and that's bad.  We'll create an iterator instead:
     71 
     72 .. code-block:: python
     73 
     74    >>> class FileIterable(object):
     75    ...     def __init__(self, filename):
     76    ...         self.filename = filename
     77    ...     def __iter__(self):
     78    ...         return FileIterator(self.filename)
     79    >>> class FileIterator(object):
     80    ...     chunk_size = 4096
     81    ...     def __init__(self, filename):
     82    ...         self.filename = filename
     83    ...         self.fileobj = open(self.filename, 'rb')
     84    ...     def __iter__(self):
     85    ...         return self
     86    ...     def next(self):
     87    ...         chunk = self.fileobj.read(self.chunk_size)
     88    ...         if not chunk:
     89    ...             raise StopIteration
     90    ...         return chunk
     91    ...     __next__ = next # py3 compat
     92    >>> def make_response(filename):
     93    ...     res = Response(content_type=get_mimetype(filename))
     94    ...     res.app_iter = FileIterable(filename)
     95    ...     res.content_length = os.path.getsize(filename)
     96    ...     return res
     97 
     98 And testing:
     99 
    100 .. code-block:: python
    101 
    102    >>> req = Request.blank('/')
    103    >>> print req.get_response(app)
    104    200 OK
    105    Content-Type: text/plain; charset=UTF-8
    106    Content-Length: 35
    107    <BLANKLINE>
    108    This is a test.  Hello test people!
    109 
    110 Well, that doesn't *look* different, but lets *imagine* that it's
    111 different because we know we changed some code.  Now to add some basic
    112 metadata to the response:
    113 
    114 .. code-block:: python
    115 
    116    >>> def make_response(filename):
    117    ...     res = Response(content_type=get_mimetype(filename),
    118    ...                    conditional_response=True)
    119    ...     res.app_iter = FileIterable(filename)
    120    ...     res.content_length = os.path.getsize(filename)
    121    ...     res.last_modified = os.path.getmtime(filename)
    122    ...     res.etag = '%s-%s-%s' % (os.path.getmtime(filename),
    123    ...                              os.path.getsize(filename), hash(filename))
    124    ...     return res
    125 
    126 Now, with ``conditional_response`` on, and with ``last_modified`` and
    127 ``etag`` set, we can do conditional requests:
    128 
    129 .. code-block:: python
    130 
    131    >>> req = Request.blank('/')
    132    >>> res = req.get_response(app)
    133    >>> print res
    134    200 OK
    135    Content-Type: text/plain; charset=UTF-8
    136    Content-Length: 35
    137    Last-Modified: ... GMT
    138    ETag: ...-...
    139    <BLANKLINE>
    140    This is a test.  Hello test people!
    141    >>> req2 = Request.blank('/')
    142    >>> req2.if_none_match = res.etag
    143    >>> req2.get_response(app)
    144    <Response ... 304 Not Modified>
    145    >>> req3 = Request.blank('/')
    146    >>> req3.if_modified_since = res.last_modified
    147    >>> req3.get_response(app)
    148    <Response ... 304 Not Modified>
    149 
    150 We can even do Range requests, but it will currently involve iterating
    151 through the file unnecessarily.  When there's a range request (and you
    152 set ``conditional_response=True``) the application will satisfy that
    153 request.  But with an arbitrary iterator the only way to do that is to
    154 run through the beginning of the iterator until you get to the chunk
    155 that the client asked for.  We can do better because we can use
    156 ``fileobj.seek(pos)`` to move around the file much more efficiently.
    157 
    158 So we'll add an extra method, ``app_iter_range``, that ``Response``
    159 looks for:
    160 
    161 .. code-block:: python
    162 
    163    >>> class FileIterable(object):
    164    ...     def __init__(self, filename, start=None, stop=None):
    165    ...         self.filename = filename
    166    ...         self.start = start
    167    ...         self.stop = stop
    168    ...     def __iter__(self):
    169    ...         return FileIterator(self.filename, self.start, self.stop)
    170    ...     def app_iter_range(self, start, stop):
    171    ...         return self.__class__(self.filename, start, stop)
    172    >>> class FileIterator(object):
    173    ...     chunk_size = 4096
    174    ...     def __init__(self, filename, start, stop):
    175    ...         self.filename = filename
    176    ...         self.fileobj = open(self.filename, 'rb')
    177    ...         if start:
    178    ...             self.fileobj.seek(start)
    179    ...         if stop is not None:
    180    ...             self.length = stop - start
    181    ...         else:
    182    ...             self.length = None
    183    ...     def __iter__(self):
    184    ...         return self
    185    ...     def next(self):
    186    ...         if self.length is not None and self.length <= 0:
    187    ...             raise StopIteration
    188    ...         chunk = self.fileobj.read(self.chunk_size)
    189    ...         if not chunk:
    190    ...             raise StopIteration
    191    ...         if self.length is not None:
    192    ...             self.length -= len(chunk)
    193    ...             if self.length < 0:
    194    ...                 # Chop off the extra:
    195    ...                 chunk = chunk[:self.length]
    196    ...         return chunk
    197    ...     __next__ = next # py3 compat
    198 
    199 Now we'll test it out:
    200 
    201 .. code-block:: python
    202 
    203    >>> req = Request.blank('/')
    204    >>> res = req.get_response(app)
    205    >>> req2 = Request.blank('/')
    206    >>> # Re-fetch the first 5 bytes:
    207    >>> req2.range = (0, 5)
    208    >>> res2 = req2.get_response(app)
    209    >>> res2
    210    <Response ... 206 Partial Content>
    211    >>> # Let's check it's our custom class:
    212    >>> res2.app_iter
    213    <FileIterable object at ...>
    214    >>> res2.body
    215    'This '
    216    >>> # Now, conditional range support:
    217    >>> req3 = Request.blank('/')
    218    >>> req3.if_range = res.etag
    219    >>> req3.range = (0, 5)
    220    >>> req3.get_response(app)
    221    <Response ... 206 Partial Content>
    222    >>> req3.if_range = 'invalid-etag'
    223    >>> req3.get_response(app)
    224    <Response ... 200 OK>
    225