Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100755 243 lines (198 sloc) 8.436 kB
fa686c0 @fjakobs update readme
fjakobs authored
1 #!/usr/bin/env python
d984ebb @joewalker added temporary static web server
joewalker authored
2 """static - A stupidly simple WSGI way to serve static (or mixed) content.
3
4 (See the docstrings of the various functions and classes.)
5
6 Copyright (C) 2006-2009 Luke Arno - http://lukearno.com/
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Lesser General Public
10 License as published by the Free Software Foundation; either
11 version 2.1 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library; if not, write to:
20
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
21 The Free Software Foundation, Inc.,
22 51 Franklin Street, Fifth Floor,
d984ebb @joewalker added temporary static web server
joewalker authored
23 Boston, MA 02110-1301, USA.
24
25 Luke Arno can be found at http://lukearno.com/
26
27 """
28
29 import mimetypes
30 import rfc822
31 import time
32 import string
33 import sys
34 from os import path, stat, getcwd
35 from wsgiref import util
36 from wsgiref.headers import Headers
37 from wsgiref.simple_server import make_server
38 from optparse import OptionParser
39
40 try: from pkg_resources import resource_filename, Requirement
41 except: pass
42
43 try: import kid
44 except: pass
45
46
47 class MagicError(Exception): pass
48
49
50 class StatusApp:
51 """Used by WSGI apps to return some HTTP status."""
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
52
d984ebb @joewalker added temporary static web server
joewalker authored
53 def __init__(self, status, message=None):
54 self.status = status
55 if message is None:
56 self.message = status
57 else:
58 self.message = message
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
59
d984ebb @joewalker added temporary static web server
joewalker authored
60 def __call__(self, environ, start_response, headers=[]):
61 if self.message:
62 Headers(headers).add_header('Content-type', 'text/plain')
63 start_response(self.status, headers)
64 if environ['REQUEST_METHOD'] == 'HEAD':
65 return [""]
66 else:
67 return [self.message]
68
69
70 class Cling(object):
71 """A stupidly simple way to serve static content via WSGI.
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
72
d984ebb @joewalker added temporary static web server
joewalker authored
73 Serve the file of the same path as PATH_INFO in self.datadir.
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
74
d984ebb @joewalker added temporary static web server
joewalker authored
75 Look up the Content-type in self.content_types by extension
76 or use 'text/plain' if the extension is not found.
77
78 Serve up the contents of the file or delegate to self.not_found.
79 """
80
81 block_size = 16 * 4096
82 index_file = 'index.html'
83 not_found = StatusApp('404 Not Found')
84 not_modified = StatusApp('304 Not Modified', "")
85 moved_permanently = StatusApp('301 Moved Permanently')
86 method_not_allowed = StatusApp('405 Method Not Allowed')
87
88 def __init__(self, root, **kw):
89 """Just set the root and any other attribs passes via **kw."""
90 self.root = root
91 for k, v in kw.iteritems():
92 setattr(self, k, v)
93
94 def __call__(self, environ, start_response):
95 """Respond to a request when called in the usual WSGI way."""
96 if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'):
97 headers = [('Allow', 'GET, HEAD')]
98 return self.method_not_allowed(environ, start_response, headers)
99 path_info = environ.get('PATH_INFO', '')
100 full_path = self._full_path(path_info)
101 if not self._is_under_root(full_path):
102 return self.not_found(environ, start_response)
103 if path.isdir(full_path):
104 if full_path[-1] <> '/' or full_path == self.root:
105 location = util.request_uri(environ, include_query=False) + '/'
106 if environ.get('QUERY_STRING'):
107 location += '?' + environ.get('QUERY_STRING')
108 headers = [('Location', location)]
109 return self.moved_permanently(environ, start_response, headers)
110 else:
111 full_path = self._full_path(path_info + self.index_file)
112 content_type = self._guess_type(full_path)
113 try:
114 etag, last_modified = self._conditions(full_path, environ)
115 headers = [('Date', rfc822.formatdate(time.time())),
116 ('Last-Modified', last_modified),
117 ('ETag', etag)]
118 if_modified = environ.get('HTTP_IF_MODIFIED_SINCE')
119 if if_modified and (rfc822.parsedate(if_modified)
120 >= rfc822.parsedate(last_modified)):
121 return self.not_modified(environ, start_response, headers)
122 if_none = environ.get('HTTP_IF_NONE_MATCH')
123 if if_none and (if_none == '*' or etag in if_none):
124 return self.not_modified(environ, start_response, headers)
125 file_like = self._file_like(full_path)
126 headers.append(('Content-Type', content_type))
127 start_response("200 OK", headers)
128 if environ['REQUEST_METHOD'] == 'GET':
129 return self._body(full_path, environ, file_like)
130 else:
131 return ['']
132 except (IOError, OSError), e:
133 print e
134 return self.not_found(environ, start_response)
135
136 def _full_path(self, path_info):
137 """Return the full path from which to read."""
138 return self.root + path_info
139
140 def _is_under_root(self, full_path):
141 """Guard against arbitrary file retrieval."""
142 if (path.abspath(full_path) + path.sep)\
143 .startswith(path.abspath(self.root) + path.sep):
144 return True
145 else:
146 return False
147
148 def _guess_type(self, full_path):
149 """Guess the mime type using the mimetypes module."""
150 return mimetypes.guess_type(full_path)[0] or 'text/plain'
151
152 def _conditions(self, full_path, environ):
153 """Return a tuple of etag, last_modified by mtime from stat."""
154 mtime = stat(full_path).st_mtime
155 return str(mtime), rfc822.formatdate(mtime)
156
157 def _file_like(self, full_path):
158 """Return the appropriate file object."""
159 return open(full_path, 'rb')
160
161 def _body(self, full_path, environ, file_like):
162 """Return an iterator over the body of the response."""
163 way_to_send = environ.get('wsgi.file_wrapper', iter_and_close)
164 return way_to_send(file_like, self.block_size)
165
166
167 def iter_and_close(file_like, block_size):
168 """Yield file contents by block then close the file."""
169 while 1:
170 try:
171 block = file_like.read(block_size)
172 if block: yield block
173 else: raise StopIteration
174 except StopIteration, si:
175 file_like.close()
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
176 return
d984ebb @joewalker added temporary static web server
joewalker authored
177
178
179 def cling_wrap(package_name, dir_name, **kw):
180 """Return a Cling that serves from the given package and dir_name.
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
181
d984ebb @joewalker added temporary static web server
joewalker authored
182 This uses pkg_resources.resource_filename which is not the
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
183 recommended way, since it extracts the files.
184
185 I think this works fine unless you have some _very_ serious
186 requirements for static content, in which case you probably
d984ebb @joewalker added temporary static web server
joewalker authored
187 shouldn't be serving it through a WSGI app, IMHO. YMMV.
188 """
189 resource = Requirement.parse(package_name)
190 return Cling(resource_filename(resource, dir_name), **kw)
191
192
193 def command():
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
194 parser = OptionParser(usage="%prog DIR [HOST][:][PORT]",
d984ebb @joewalker added temporary static web server
joewalker authored
195 version="static 0.3.6")
196 options, args = parser.parse_args()
197 if len(args) in (1, 2):
198 if len(args) == 2:
199 parts = args[1].split(":")
200 if len(parts) == 1:
201 host = parts[0]
202 port = None
203 elif len(parts) == 2:
204 host, port = parts
205 else:
206 sys.exit("Invalid host:port specification.")
207 elif len(args) == 1:
208 host, port = None, None
209 if not host:
210 host = '0.0.0.0'
211 if not port:
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
212 port = 8888
d984ebb @joewalker added temporary static web server
joewalker authored
213 try:
214 port = int(port)
215 except:
216 sys.exit("Invalid host:port specification.")
217 app = Cling(args[0])
218 try:
219 make_server(host, port, app).serve_forever()
220 except KeyboardInterrupt, ki:
221 print "Cio, baby!"
222 except:
223 sys.exit("Problem initializing server.")
224 else:
225 parser.print_help(sys.stderr)
226 sys.exit(1)
227
228
229 def test():
230 from wsgiref.validate import validator
231 app = Cling(getcwd())
232 try:
bc93a71 @jviereck Update Readme. Make static.py serve on port 8888 like static.js does.
jviereck authored
233 print "Serving " + getcwd() + " to http://localhost:8888"
234 make_server('0.0.0.0', 8888, validator(app)).serve_forever()
d984ebb @joewalker added temporary static web server
joewalker authored
235 except KeyboardInterrupt, ki:
236 print ""
237 print "Ciao, baby!"
238
239
240 if __name__ == '__main__':
241 test()
242
Something went wrong with that request. Please try again.