WebOb File-Serving Example
==========================

This document shows how you can make a static-file-serving application
using WebOb.  We'll quickly build this up from minimal functionality
to a high-quality file serving application.

.. comment:

   >>> import webob, os
   >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__))
   >>> doc_dir = os.path.join(base_dir, 'docs')
   >>> from dtopt import ELLIPSIS

First we'll setup a really simple shim around our application, which
we can use as we improve our application:

.. code-block::

   >>> from webob import Request, Response, UTC
   >>> import os
   >>> class FileApp(object):
   ...     def __init__(self, filename):
   ...         self.filename = filename
   ...     def __call__(self, environ, start_response):
   ...         res = make_response(self.filename)
   ...         return res(environ, start_response)
   >>> import mimetypes
   >>> def get_mimetype(filename):
   ...     type, encoding = mimetypes.guess_type(filename)
   ...     # We'll ignore encoding
   ...     return type or 'application/octet-stream'

Now we can make different definitions of ``make_response``.  The
simplest version:

.. code-block::

   >>> import mimetypes
   >>> def make_response(filename):
   ...     res = Response(content_type=get_mimetype(filename))
   ...     res.body = open(filename, 'rb').read()
   ...     return res

Let's give it a go.  We'll test it out with a file ``test-file.txt``
in the WebOb doc directory:

.. code-block::

   >>> fn = os.path.join(doc_dir, 'test-file.txt')
   >>> open(fn).read()
   'This is a test.  Hello test people!\n'
   >>> app = FileApp(fn)
   >>> req = Request.blank('/')
   >>> print req.get_response(app)
   200 OK
   content-type: text/plain
   Content-Length: 36
   <BLANKLINE>
   This is a test.  Hello test people!
   <BLANKLINE>

Well, that worked.  But it's not a very fancy object.  First, it reads
everything into memory, and that's bad.  We'll create an iterator instead:

.. code-block::

   >>> class FileIterable(object):
   ...     def __init__(self, filename):
   ...         self.filename = filename
   ...     def __iter__(self):
   ...         return FileIterator(self.filename)
   >>> class FileIterator(object):
   ...     chunk_size = 4096
   ...     def __init__(self, filename):
   ...         self.filename = filename
   ...         self.fileobj = open(self.filename, 'rb')
   ...     def __iter__(self):
   ...         return self
   ...     def next(self):
   ...         chunk = self.fileobj.read(self.chunk_size)
   ...         if not chunk:
   ...             raise StopIteration
   ...         return chunk
   >>> def make_response(filename):
   ...     res = Response(content_type=get_mimetype(filename))
   ...     res.app_iter = FileIterable(filename)
   ...     res.content_length = os.path.getsize(filename)
   ...     return res

And testing:

.. code-block::

   >>> req = Request.blank('/')
   >>> print req.get_response(app)
   200 OK
   content-type: text/plain
   Content-Length: 36
   <BLANKLINE>
   This is a test.  Hello test people!
   <BLANKLINE>

Well, that doesn't *look* different, but lets *imagine* that it's
different because we know we did stuff differently.  Now to add some
basic data:

.. code-block::

   >>> def make_response(filename):
   ...     res = Response(content_type=get_mimetype(filename),
   ...                    conditional_response=True)
   ...     res.app_iter = FileIterable(filename)
   ...     res.content_length = os.path.getsize(filename)
   ...     res.last_modified = os.path.getmtime(filename)
   ...     res.etag = '%s-%s' % (os.path.getmtime(filename), hash(filename))
   ...     return res

Now, with ``conditional_response`` on, and with ``last_modified`` and
``etag`` set, we can do conditional requests:

.. code-block::

   >>> req = Request.blank('/')
   >>> res = req.get_response(app)
   >>> print res
   200 OK
   content-type: text/plain
   Content-Length: 36
   Last-Modified: ... GMT
   ETag: ...-...
   <BLANKLINE>
   This is a test.  Hello test people!
   <BLANKLINE>
   >>> req2 = Request.blank('/')
   >>> req2.if_none_match = res.etag
   >>> req2.get_response(app)
   <Response ... 304 Not Modified>
   >>> req3 = Request.blank('/')
   >>> req3.if_modified_since = res.last_modified
   >>> req3.get_response(app)
   <Response ... 304 Not Modified>
   
We can even do Range requests, but it will currently involve iterating
through the file unnecessarily, because we can use
``fileobj.seek(pos)`` to move around the file much more efficiently.

.. code-block::

   >>> class FileIterable(object):
   ...     def __init__(self, filename, start=None, stop=None):
   ...         self.filename = filename
   ...         self.start = start
   ...         self.stop = stop
   ...     def __iter__(self):
   ...         return FileIterator(self.filename, self.start, self.stop)
   ...     def app_iter_range(self, start, stop):
   ...         return self.__class__(self.filename, start, stop)
   >>> class FileIterator(object):
   ...     chunk_size = 4096
   ...     def __init__(self, filename, start, stop):
   ...         self.filename = filename
   ...         self.fileobj = open(self.filename, 'rb')
   ...         if start:
   ...             self.fileobj.seek(start)
   ...         if stop is not None:
   ...             self.length = stop - start
   ...         else:
   ...             self.length = None
   ...     def __iter__(self):
   ...         return self
   ...     def next(self):
   ...         if self.length is not None and self.length <= 0:
   ...             raise StopIteration
   ...         chunk = self.fileobj.read(self.chunk_size)
   ...         if not chunk:
   ...             raise StopIteration
   ...         if self.length is not None:
   ...             self.length -= len(chunk)
   ...             if self.length < 0:
   ...                 # Chop off the extra:
   ...                 chunk = chunk[:self.length]
   ...         return chunk

Now we'll test it out:

.. code-block::

   >>> req = Request.blank('/')
   >>> res = req.get_response(app)
   >>> req2 = Request.blank('/')
   >>> # Re-fetch the first 5 bytes:
   >>> req2.range = (0, 5)
   >>> res2 = req2.get_response(app)
   >>> res2
   <Response ... 206 Partial Content>
   >>> # Let's check it's our custom class:
   >>> res2.app_iter
   <FileIterable object at ...>
   >>> res2.body
   'This '
   >>> # Now, conditional range support:
   >>> req3 = Request.blank('/')
   >>> req3.if_range = res.etag
   >>> req3.range = (0, 5)
   >>> req3.get_response(app)
   <Response ... 206 Partial Content>
   >>> req3.if_range = 'invalid-etag'
   >>> req3.get_response(app)
   <Response ... 200 OK>
   