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