|
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> |