Add google appengine to repo
[frozenviper.git] / google_appengine / google / appengine / ext / zipserve / __init__.py
blob2da833e04ee7da1a5b761291c42c51c84af80575
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
18 """Serve static files from a zipfile.
20 This is a solution for apps that want to serve 1000s of small static
21 files while staying withing the 1000 file limit.
23 The simplest use case is driven purely from the handlers section in
24 app.yaml, e.g.:
26 - url: /images/.*
27 script: $PYTHON_LIB/google/appengine/ext/zipserve
29 This would invoke a main() within zipserve/__init__.py. This code
30 would then take the URL path, and look for a .zip file under the first
31 component of the path, in this case "images.zip" in the app's working
32 directory. If found, it will then serve any matching paths below that
33 from the zip file. In other words, /images/foo/icon.gif would map to
34 foo/icon.gif in the zip file images.zip.
36 You can also customize the behavior by adding a custom line to your
37 WSGIApplication() invocation:
39 def main():
40 app = webapp.WSGIApplication(
41 [('/', MainPage),
42 ('/static/(.*)', zipserve.make_zip_handler('staticfiles.zip')),
45 You can pass max_age=N to the make_zip_handler() call to override the
46 expiration time in seconds, which defaults to 600.
48 To customize the behavior even more, you can subclass ZipHandler and
49 override the get() method, or override it and call ServeFromZipFile()
50 directly.
52 Note that by default, a Cache-control is added that makes these pages
53 cacheable even if they require authentication. If this is not what
54 you want, override ZipHandler.SetCachingHeaders().
55 """
58 import email.Utils
59 import logging
60 import mimetypes
61 import time
62 import zipfile
64 from google.appengine.ext import webapp
65 from google.appengine.ext.webapp import util
68 def make_zip_handler(zipfilename, max_age=None, public=None):
69 """Factory function to construct a custom ZipHandler instance.
71 Args:
72 zipfilename: The filename of a zipfile.
73 max_age: Optional expiration time; defaults to ZipHandler.MAX_AGE.
74 public: Optional public flag; defaults to ZipHandler.PUBLIC.
76 Returns:
77 A ZipHandler subclass.
78 """
79 class CustomZipHandler(ZipHandler):
80 def get(self, name):
81 self.ServeFromZipFile(self.ZIPFILENAME, name)
82 ZIPFILENAME = zipfilename
83 if max_age is not None:
84 MAX_AGE = max_age
85 if public is not None:
86 PUBLIC = public
88 return CustomZipHandler
91 class ZipHandler(webapp.RequestHandler):
92 """Request handler serving static files from zipfiles."""
94 zipfile_cache = {}
96 def get(self, prefix, name):
97 """GET request handler.
99 Typically the arguments are passed from the matching groups in the
100 URL pattern passed to WSGIApplication().
102 Args:
103 prefix: The zipfilename without the .zip suffix.
104 name: The name within the zipfile.
106 self.ServeFromZipFile(prefix + '.zip', name)
108 def ServeFromZipFile(self, zipfilename, name):
109 """Helper for the GET request handler.
111 This serves the contents of file 'name' from zipfile
112 'zipfilename', logging a message and returning a 404 response if
113 either the zipfile cannot be opened or the named file cannot be
114 read from it.
116 Args:
117 zipfilename: The name of the zipfile.
118 name: The name within the zipfile.
120 zipfile_object = self.zipfile_cache.get(zipfilename)
121 if zipfile_object is None:
122 try:
123 zipfile_object = zipfile.ZipFile(zipfilename)
124 except (IOError, RuntimeError), err:
125 logging.error('Can\'t open zipfile %s: %s', zipfilename, err)
126 zipfile_object = ''
127 self.zipfile_cache[zipfilename] = zipfile_object
128 if zipfile_object == '':
129 self.error(404)
130 self.response.out.write('Not found')
131 return
132 try:
133 data = zipfile_object.read(name)
134 except (KeyError, RuntimeError), err:
135 self.error(404)
136 self.response.out.write('Not found')
137 return
138 content_type, encoding = mimetypes.guess_type(name)
139 if content_type:
140 self.response.headers['Content-Type'] = content_type
141 self.SetCachingHeaders()
142 self.response.out.write(data)
144 MAX_AGE = 600
146 PUBLIC = True
148 def SetCachingHeaders(self):
149 """Helper to set the caching headers.
151 Override this to customize the headers beyond setting MAX_AGE.
153 max_age = self.MAX_AGE
154 self.response.headers['Expires'] = email.Utils.formatdate(
155 time.time() + max_age, usegmt=True)
156 cache_control = []
157 if self.PUBLIC:
158 cache_control.append('public')
159 cache_control.append('max-age=%d' % max_age)
160 self.response.headers['Cache-Control'] = ', '.join(cache_control)
163 def main():
164 """Main program.
166 This is invoked when this package is referenced from app.yaml.
168 application = webapp.WSGIApplication([('/([^/]+)/(.*)', ZipHandler)])
169 util.run_wsgi_app(application)
172 if __name__ == '__main__':
173 main()