diff options
Diffstat (limited to 'scripts/app_engine_server')
-rw-r--r-- | scripts/app_engine_server/LICENSE | 202 | ||||
-rwxr-xr-x | scripts/app_engine_server/app.yaml | 16 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/README | 17 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/__init__.py | 0 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/__init__.pyc | bin | 0 -> 216 bytes | |||
-rwxr-xr-x | scripts/app_engine_server/gae_shell/shell.py | 308 | ||||
-rwxr-xr-x | scripts/app_engine_server/gae_shell/shell.py~ | 308 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/static/shell.js | 195 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/static/spinner.gif | bin | 0 -> 1514 bytes | |||
-rw-r--r-- | scripts/app_engine_server/gae_shell/templates/shell.html | 122 | ||||
-rw-r--r-- | scripts/app_engine_server/index.yaml | 12 | ||||
-rw-r--r-- | scripts/app_engine_server/memcache_zipserve.py | 412 |
12 files changed, 1592 insertions, 0 deletions
diff --git a/scripts/app_engine_server/LICENSE b/scripts/app_engine_server/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/scripts/app_engine_server/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/app_engine_server/app.yaml b/scripts/app_engine_server/app.yaml new file mode 100755 index 0000000..1fb50c7 --- /dev/null +++ b/scripts/app_engine_server/app.yaml @@ -0,0 +1,16 @@ +application: androidappdocs-staging +version: 1 +runtime: python +api_version: 1 + +handlers: +- url: /gae_shell/static + static_dir: gae_shell/static + expiration: 1d + +- url: /gae_shell/.* + script: /gae_shell/shell.py + login: admin + +- url: .* + script: main.py diff --git a/scripts/app_engine_server/gae_shell/README b/scripts/app_engine_server/gae_shell/README new file mode 100644 index 0000000..5b0089f --- /dev/null +++ b/scripts/app_engine_server/gae_shell/README @@ -0,0 +1,17 @@ +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. diff --git a/scripts/app_engine_server/gae_shell/__init__.py b/scripts/app_engine_server/gae_shell/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/__init__.py diff --git a/scripts/app_engine_server/gae_shell/__init__.pyc b/scripts/app_engine_server/gae_shell/__init__.pyc Binary files differnew file mode 100644 index 0000000..84951e9 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/__init__.pyc diff --git a/scripts/app_engine_server/gae_shell/shell.py b/scripts/app_engine_server/gae_shell/shell.py new file mode 100755 index 0000000..df2fb17 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/shell.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# +# Copyright 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. + +TODO: unit tests! +""" + +import logging +import new +import os +import pickle +import sys +import traceback +import types +import wsgiref.handlers + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + + +# Set to True if stack traces should be shown in the browser, etc. +_DEBUG = True + +# The entity kind for shell sessions. Feel free to rename to suit your app. +_SESSION_KIND = '_Shell_Session' + +# Types that can't be pickled. +UNPICKLABLE_TYPES = ( + types.ModuleType, + types.TypeType, + types.ClassType, + types.FunctionType, + ) + +# Unpicklable statements to seed new sessions with. +INITIAL_UNPICKLABLES = [ + 'import logging', + 'import os', + 'import sys', + 'from google.appengine.ext import db', + 'from google.appengine.api import users', + ] + + +class Session(db.Model): + """A shell session. Stores the session's globals. + + Each session globals is stored in one of two places: + + If the global is picklable, it's stored in the parallel globals and + global_names list properties. (They're parallel lists to work around the + unfortunate fact that the datastore can't store dictionaries natively.) + + If the global is not picklable (e.g. modules, classes, and functions), or if + it was created by the same statement that created an unpicklable global, + it's not stored directly. Instead, the statement is stored in the + unpicklables list property. On each request, before executing the current + statement, the unpicklable statements are evaluated to recreate the + unpicklable globals. + + The unpicklable_names property stores all of the names of globals that were + added by unpicklable statements. When we pickle and store the globals after + executing a statement, we skip the ones in unpicklable_names. + + Using Text instead of string is an optimization. We don't query on any of + these properties, so they don't need to be indexed. + """ + global_names = db.ListProperty(db.Text) + globals = db.ListProperty(db.Blob) + unpicklable_names = db.ListProperty(db.Text) + unpicklables = db.ListProperty(db.Text) + + def set_global(self, name, value): + """Adds a global, or updates it if it already exists. + + Also removes the global from the list of unpicklable names. + + Args: + name: the name of the global to remove + value: any picklable value + """ + blob = db.Blob(pickle.dumps(value)) + + if name in self.global_names: + index = self.global_names.index(name) + self.globals[index] = blob + else: + self.global_names.append(db.Text(name)) + self.globals.append(blob) + + self.remove_unpicklable_name(name) + + def remove_global(self, name): + """Removes a global, if it exists. + + Args: + name: string, the name of the global to remove + """ + if name in self.global_names: + index = self.global_names.index(name) + del self.global_names[index] + del self.globals[index] + + def globals_dict(self): + """Returns a dictionary view of the globals. + """ + return dict((name, pickle.loads(val)) + for name, val in zip(self.global_names, self.globals)) + + def add_unpicklable(self, statement, names): + """Adds a statement and list of names to the unpicklables. + + Also removes the names from the globals. + + Args: + statement: string, the statement that created new unpicklable global(s). + names: list of strings; the names of the globals created by the statement. + """ + self.unpicklables.append(db.Text(statement)) + + for name in names: + self.remove_global(name) + if name not in self.unpicklable_names: + self.unpicklable_names.append(db.Text(name)) + + def remove_unpicklable_name(self, name): + """Removes a name from the list of unpicklable names, if it exists. + + Args: + name: string, the name of the unpicklable global to remove + """ + if name in self.unpicklable_names: + self.unpicklable_names.remove(name) + + +class FrontPageHandler(webapp.RequestHandler): + """Creates a new session and renders the shell.html template. + """ + + def get(self): + # set up the session. TODO: garbage collect old shell sessions + session_key = self.request.get('session') + if session_key: + session = Session.get(session_key) + else: + # create a new session + session = Session() + session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES] + session_key = session.put() + + template_file = os.path.join(os.path.dirname(__file__), 'templates', + 'shell.html') + session_url = '/?session=%s' % session_key + vars = { 'server_software': os.environ['SERVER_SOFTWARE'], + 'python_version': sys.version, + 'session': str(session_key), + 'user': users.get_current_user(), + 'login_url': users.create_login_url(session_url), + 'logout_url': users.create_logout_url(session_url), + } + rendered = webapp.template.render(template_file, vars, debug=_DEBUG) + self.response.out.write(rendered) + + +class StatementHandler(webapp.RequestHandler): + """Evaluates a python statement in a given session and returns the result. + """ + + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + + # extract the statement to be run + statement = self.request.get('statement') + if not statement: + return + + # the python compiler doesn't like network line endings + statement = statement.replace('\r\n', '\n') + + # add a couple newlines at the end of the statement. this makes + # single-line expressions such as 'class Foo: pass' evaluate happily. + statement += '\n\n' + + # log and compile the statement up front + try: + logging.info('Compiling and evaluating:\n%s' % statement) + compiled = compile(statement, '<string>', 'single') + except: + self.response.out.write(traceback.format_exc()) + return + + # create a dedicated module to be used as this statement's __main__ + statement_module = new.module('__main__') + + # use this request's __builtin__, since it changes on each request. + # this is needed for import statements, among other things. + import __builtin__ + statement_module.__builtins__ = __builtin__ + + # load the session from the datastore + session = Session.get(self.request.get('session')) + + # swap in our custom module for __main__. then unpickle the session + # globals, run the statement, and re-pickle the session globals, all + # inside it. + old_main = sys.modules.get('__main__') + try: + sys.modules['__main__'] = statement_module + statement_module.__name__ = '__main__' + + # re-evaluate the unpicklables + for code in session.unpicklables: + exec code in statement_module.__dict__ + + # re-initialize the globals + for name, val in session.globals_dict().items(): + try: + statement_module.__dict__[name] = val + except: + msg = 'Dropping %s since it could not be unpickled.\n' % name + self.response.out.write(msg) + logging.warning(msg + traceback.format_exc()) + session.remove_global(name) + + # run! + old_globals = dict(statement_module.__dict__) + try: + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = self.response.out + sys.stderr = self.response.out + exec compiled in statement_module.__dict__ + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + except: + self.response.out.write(traceback.format_exc()) + return + + # extract the new globals that this statement added + new_globals = {} + for name, val in statement_module.__dict__.items(): + if name not in old_globals or val != old_globals[name]: + new_globals[name] = val + + if True in [isinstance(val, UNPICKLABLE_TYPES) + for val in new_globals.values()]: + # this statement added an unpicklable global. store the statement and + # the names of all of the globals it added in the unpicklables. + session.add_unpicklable(statement, new_globals.keys()) + logging.debug('Storing this statement as an unpicklable.') + + else: + # this statement didn't add any unpicklables. pickle and store the + # new globals back into the datastore. + for name, val in new_globals.items(): + if not name.startswith('__'): + session.set_global(name, val) + + finally: + sys.modules['__main__'] = old_main + + session.put() + + +def main(): + application = webapp.WSGIApplication( + [('/gae_shell/', FrontPageHandler), + ('/gae_shell/shell.do', StatementHandler)], debug=_DEBUG) + wsgiref.handlers.CGIHandler().run(application) + + +if __name__ == '__main__': + main() diff --git a/scripts/app_engine_server/gae_shell/shell.py~ b/scripts/app_engine_server/gae_shell/shell.py~ new file mode 100755 index 0000000..dee9fdb --- /dev/null +++ b/scripts/app_engine_server/gae_shell/shell.py~ @@ -0,0 +1,308 @@ +#!/usr/bin/python +# +# Copyright 2007 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +An interactive, stateful AJAX shell that runs Python code on the server. + +Part of http://code.google.com/p/google-app-engine-samples/. + +May be run as a standalone app or in an existing app as an admin-only handler. +Can be used for system administration tasks, as an interactive way to try out +APIs, or as a debugging aid during development. + +The logging, os, sys, db, and users modules are imported automatically. + +Interpreter state is stored in the datastore so that variables, function +definitions, and other values in the global and local namespaces can be used +across commands. + +To use the shell in your app, copy shell.py, static/*, and templates/* into +your app's source directory. Then, copy the URL handlers from app.yaml into +your app.yaml. + +TODO: unit tests! +""" + +import logging +import new +import os +import pickle +import sys +import traceback +import types +import wsgiref.handlers + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + + +# Set to True if stack traces should be shown in the browser, etc. +_DEBUG = True + +# The entity kind for shell sessions. Feel free to rename to suit your app. +_SESSION_KIND = '_Shell_Session' + +# Types that can't be pickled. +UNPICKLABLE_TYPES = ( + types.ModuleType, + types.TypeType, + types.ClassType, + types.FunctionType, + ) + +# Unpicklable statements to seed new sessions with. +INITIAL_UNPICKLABLES = [ + 'import logging', + 'import os', + 'import sys', + 'from google.appengine.ext import db', + 'from google.appengine.api import users', + ] + + +class Session(db.Model): + """A shell session. Stores the session's globals. + + Each session globals is stored in one of two places: + + If the global is picklable, it's stored in the parallel globals and + global_names list properties. (They're parallel lists to work around the + unfortunate fact that the datastore can't store dictionaries natively.) + + If the global is not picklable (e.g. modules, classes, and functions), or if + it was created by the same statement that created an unpicklable global, + it's not stored directly. Instead, the statement is stored in the + unpicklables list property. On each request, before executing the current + statement, the unpicklable statements are evaluated to recreate the + unpicklable globals. + + The unpicklable_names property stores all of the names of globals that were + added by unpicklable statements. When we pickle and store the globals after + executing a statement, we skip the ones in unpicklable_names. + + Using Text instead of string is an optimization. We don't query on any of + these properties, so they don't need to be indexed. + """ + global_names = db.ListProperty(db.Text) + globals = db.ListProperty(db.Blob) + unpicklable_names = db.ListProperty(db.Text) + unpicklables = db.ListProperty(db.Text) + + def set_global(self, name, value): + """Adds a global, or updates it if it already exists. + + Also removes the global from the list of unpicklable names. + + Args: + name: the name of the global to remove + value: any picklable value + """ + blob = db.Blob(pickle.dumps(value)) + + if name in self.global_names: + index = self.global_names.index(name) + self.globals[index] = blob + else: + self.global_names.append(db.Text(name)) + self.globals.append(blob) + + self.remove_unpicklable_name(name) + + def remove_global(self, name): + """Removes a global, if it exists. + + Args: + name: string, the name of the global to remove + """ + if name in self.global_names: + index = self.global_names.index(name) + del self.global_names[index] + del self.globals[index] + + def globals_dict(self): + """Returns a dictionary view of the globals. + """ + return dict((name, pickle.loads(val)) + for name, val in zip(self.global_names, self.globals)) + + def add_unpicklable(self, statement, names): + """Adds a statement and list of names to the unpicklables. + + Also removes the names from the globals. + + Args: + statement: string, the statement that created new unpicklable global(s). + names: list of strings; the names of the globals created by the statement. + """ + self.unpicklables.append(db.Text(statement)) + + for name in names: + self.remove_global(name) + if name not in self.unpicklable_names: + self.unpicklable_names.append(db.Text(name)) + + def remove_unpicklable_name(self, name): + """Removes a name from the list of unpicklable names, if it exists. + + Args: + name: string, the name of the unpicklable global to remove + """ + if name in self.unpicklable_names: + self.unpicklable_names.remove(name) + + +class FrontPageHandler(webapp.RequestHandler): + """Creates a new session and renders the shell.html template. + """ + + def get(self): + # set up the session. TODO: garbage collect old shell sessions + session_key = self.request.get('session') + if session_key: + session = Session.get(session_key) + else: + # create a new session + session = Session() + session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES] + session_key = session.put() + + template_file = os.path.join(os.path.dirname(__file__), 'templates', + 'shell.html') + session_url = '/?session=%s' % session_key + vars = { 'server_software': os.environ['SERVER_SOFTWARE'], + 'python_version': sys.version, + 'session': str(session_key), + 'user': users.get_current_user(), + 'login_url': users.create_login_url(session_url), + 'logout_url': users.create_logout_url(session_url), + } + rendered = webapp.template.render(template_file, vars, debug=_DEBUG) + self.response.out.write(rendered) + + +class StatementHandler(webapp.RequestHandler): + """Evaluates a python statement in a given session and returns the result. + """ + + def get(self): + self.response.headers['Content-Type'] = 'text/plain' + + # extract the statement to be run + statement = self.request.get('statement') + if not statement: + return + + # the python compiler doesn't like network line endings + statement = statement.replace('\r\n', '\n') + + # add a couple newlines at the end of the statement. this makes + # single-line expressions such as 'class Foo: pass' evaluate happily. + statement += '\n\n' + + # log and compile the statement up front + try: + logging.info('Compiling and evaluating:\n%s' % statement) + compiled = compile(statement, '<string>', 'single') + except: + self.response.out.write(traceback.format_exc()) + return + + # create a dedicated module to be used as this statement's __main__ + statement_module = new.module('__main__') + + # use this request's __builtin__, since it changes on each request. + # this is needed for import statements, among other things. + import __builtin__ + statement_module.__builtins__ = __builtin__ + + # load the session from the datastore + session = Session.get(self.request.get('session')) + + # swap in our custom module for __main__. then unpickle the session + # globals, run the statement, and re-pickle the session globals, all + # inside it. + old_main = sys.modules.get('__main__') + try: + sys.modules['__main__'] = statement_module + statement_module.__name__ = '__main__' + + # re-evaluate the unpicklables + for code in session.unpicklables: + exec code in statement_module.__dict__ + + # re-initialize the globals + for name, val in session.globals_dict().items(): + try: + statement_module.__dict__[name] = val + except: + msg = 'Dropping %s since it could not be unpickled.\n' % name + self.response.out.write(msg) + logging.warning(msg + traceback.format_exc()) + session.remove_global(name) + + # run! + old_globals = dict(statement_module.__dict__) + try: + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = self.response.out + sys.stderr = self.response.out + exec compiled in statement_module.__dict__ + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + except: + self.response.out.write(traceback.format_exc()) + return + + # extract the new globals that this statement added + new_globals = {} + for name, val in statement_module.__dict__.items(): + if name not in old_globals or val != old_globals[name]: + new_globals[name] = val + + if True in [isinstance(val, UNPICKLABLE_TYPES) + for val in new_globals.values()]: + # this statement added an unpicklable global. store the statement and + # the names of all of the globals it added in the unpicklables. + session.add_unpicklable(statement, new_globals.keys()) + logging.debug('Storing this statement as an unpicklable.') + + else: + # this statement didn't add any unpicklables. pickle and store the + # new globals back into the datastore. + for name, val in new_globals.items(): + if not name.startswith('__'): + session.set_global(name, val) + + finally: + sys.modules['__main__'] = old_main + + session.put() + + +def main(): + application = webapp.WSGIApplication( + [('/', FrontPageHandler), + ('/shell.do', StatementHandler)], debug=_DEBUG) + wsgiref.handlers.CGIHandler().run(application) + + +if __name__ == '__main__': + main() diff --git a/scripts/app_engine_server/gae_shell/static/shell.js b/scripts/app_engine_server/gae_shell/static/shell.js new file mode 100644 index 0000000..4aa1583 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/static/shell.js @@ -0,0 +1,195 @@ +// Copyright 2007 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview + * Javascript code for the interactive AJAX shell. + * + * Part of http://code.google.com/p/google-app-engine-samples/. + * + * Includes a function (shell.runStatement) that sends the current python + * statement in the shell prompt text box to the server, and a callback + * (shell.done) that displays the results when the XmlHttpRequest returns. + * + * Also includes cross-browser code (shell.getXmlHttpRequest) to get an + * XmlHttpRequest. + */ + +/** + * Shell namespace. + * @type {Object} + */ +var shell = {} + +/** + * The shell history. history is an array of strings, ordered oldest to + * newest. historyCursor is the current history element that the user is on. + * + * The last history element is the statement that the user is currently + * typing. When a statement is run, it's frozen in the history, a new history + * element is added to the end of the array for the new statement, and + * historyCursor is updated to point to the new element. + * + * @type {Array} + */ +shell.history = ['']; + +/** + * See {shell.history} + * @type {number} + */ +shell.historyCursor = 0; + +/** + * A constant for the XmlHttpRequest 'done' state. + * @type Number + */ +shell.DONE_STATE = 4; + +/** + * A cross-browser function to get an XmlHttpRequest object. + * + * @return {XmlHttpRequest?} a new XmlHttpRequest + */ +shell.getXmlHttpRequest = function() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } else if (window.ActiveXObject) { + try { + return new ActiveXObject('Msxml2.XMLHTTP'); + } catch(e) { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + } + + return null; +}; + +/** + * This is the prompt textarea's onkeypress handler. Depending on the key that + * was pressed, it will run the statement, navigate the history, or update the + * current statement in the history. + * + * @param {Event} event the keypress event + * @return {Boolean} false to tell the browser not to submit the form. + */ +shell.onPromptKeyPress = function(event) { + var statement = document.getElementById('statement'); + + if (this.historyCursor == this.history.length - 1) { + // we're on the current statement. update it in the history before doing + // anything. + this.history[this.historyCursor] = statement.value; + } + + // should we pull something from the history? + if (event.ctrlKey && event.keyCode == 38 /* up arrow */) { + if (this.historyCursor > 0) { + statement.value = this.history[--this.historyCursor]; + } + return false; + } else if (event.ctrlKey && event.keyCode == 40 /* down arrow */) { + if (this.historyCursor < this.history.length - 1) { + statement.value = this.history[++this.historyCursor]; + } + return false; + } else if (!event.altKey) { + // probably changing the statement. update it in the history. + this.historyCursor = this.history.length - 1; + this.history[this.historyCursor] = statement.value; + } + + // should we submit? + var ctrlEnter = (document.getElementById('submit_key').value == 'ctrl-enter'); + if (event.keyCode == 13 /* enter */ && !event.altKey && !event.shiftKey && + event.ctrlKey == ctrlEnter) { + return this.runStatement(); + } +}; + +/** + * The XmlHttpRequest callback. If the request succeeds, it adds the command + * and its resulting output to the shell history div. + * + * @param {XmlHttpRequest} req the XmlHttpRequest we used to send the current + * statement to the server + */ +shell.done = function(req) { + if (req.readyState == this.DONE_STATE) { + var statement = document.getElementById('statement') + statement.className = 'prompt'; + + // add the command to the shell output + var output = document.getElementById('output'); + + output.value += '\n>>> ' + statement.value; + statement.value = ''; + + // add a new history element + this.history.push(''); + this.historyCursor = this.history.length - 1; + + // add the command's result + var result = req.responseText.replace(/^\s*|\s*$/g, ''); // trim whitespace + if (result != '') + output.value += '\n' + result; + + // scroll to the bottom + output.scrollTop = output.scrollHeight; + if (output.createTextRange) { + var range = output.createTextRange(); + range.collapse(false); + range.select(); + } + } +}; + +/** + * This is the form's onsubmit handler. It sends the python statement to the + * server, and registers shell.done() as the callback to run when it returns. + * + * @return {Boolean} false to tell the browser not to submit the form. + */ +shell.runStatement = function() { + var form = document.getElementById('form'); + + // build a XmlHttpRequest + var req = this.getXmlHttpRequest(); + if (!req) { + document.getElementById('ajax-status').innerHTML = + "<span class='error'>Your browser doesn't support AJAX. :(</span>"; + return false; + } + + req.onreadystatechange = function() { shell.done(req); }; + + // build the query parameter string + var params = ''; + for (i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type != 'submit' && elem.type != 'button' && elem.id != 'caret') { + var value = escape(elem.value).replace(/\+/g, '%2B'); // escape ignores + + params += '&' + elem.name + '=' + value; + } + } + + // send the request and tell the user. + document.getElementById('statement').className = 'prompt processing'; + req.open(form.method, form.action + '?' + params, true); + req.setRequestHeader('Content-type', + 'application/x-www-form-urlencoded;charset=UTF-8'); + req.send(null); + + return false; +}; diff --git a/scripts/app_engine_server/gae_shell/static/spinner.gif b/scripts/app_engine_server/gae_shell/static/spinner.gif Binary files differnew file mode 100644 index 0000000..3e58d6e --- /dev/null +++ b/scripts/app_engine_server/gae_shell/static/spinner.gif diff --git a/scripts/app_engine_server/gae_shell/templates/shell.html b/scripts/app_engine_server/gae_shell/templates/shell.html new file mode 100644 index 0000000..123b200 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/templates/shell.html @@ -0,0 +1,122 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<title> Interactive Shell </title> +<script type="text/javascript" src="/gae_shell/static/shell.js"></script> +<style type="text/css"> +body { + font-family: monospace; + font-size: 10pt; +} + +p { + margin: 0.5em; +} + +.prompt, #output { + width: 45em; + border: 1px solid silver; + background-color: #f5f5f5; + font-size: 10pt; + margin: 0.5em; + padding: 0.5em; + padding-right: 0em; + overflow-x: hidden; +} + +#toolbar { + margin-left: 0.5em; + padding-left: 0.5em; +} + +#caret { + width: 2.5em; + margin-right: 0px; + padding-right: 0px; + border-right: 0px; +} + +#statement { + width: 43em; + margin-left: -1em; + padding-left: 0px; + border-left: 0px; + background-position: top right; + background-repeat: no-repeat; +} + +.processing { + background-image: url("/gae_shell/static/spinner.gif"); +} + +#ajax-status { + font-weight: bold; +} + +.message { + color: #8AD; + font-weight: bold; + font-style: italic; +} + +.error { + color: #F44; +} + +.username { + font-weight: bold; +} + +</style> +</head> + +<body> + +<p> Interactive server-side Python shell for +<a href="http://code.google.com/appengine/">Google App Engine</a>. +(<a href="http://code.google.com/p/google-app-engine-samples/">source</a>) +</p> + +<textarea id="output" rows="22" readonly="readonly"> +{{ server_software }} +Python {{ python_version }} +</textarea> + +<form id="form" action="shell.do" method="get"> + <nobr> + <textarea class="prompt" id="caret" readonly="readonly" rows="4" + onfocus="document.getElementById('statement').focus()" + >>>></textarea> + <textarea class="prompt" name="statement" id="statement" rows="4" + onkeypress="return shell.onPromptKeyPress(event);"></textarea> + </nobr> + <input type="hidden" name="session" value="{{ session }}" /> + <input type="submit" style="display: none" /> +</form> + +<p id="ajax-status"></p> + +<p id="toolbar"> +{% if user %} + <span class="username">{{ user.nickname }}</span> + (<a href="{{ logout_url }}">log out</a>) +{% else %} + <a href="{{ login_url }}">log in</a> +{% endif %} + | Ctrl-Up/Down for history | +<select id="submit_key"> + <option value="enter">Enter</option> + <option value="ctrl-enter" selected="selected">Ctrl-Enter</option> +</select> +<label for="submit_key">submits</label> +</p> + +<script type="text/javascript"> +document.getElementById('statement').focus(); +</script> + +</body> +</html> + diff --git a/scripts/app_engine_server/index.yaml b/scripts/app_engine_server/index.yaml new file mode 100644 index 0000000..8e6046d --- /dev/null +++ b/scripts/app_engine_server/index.yaml @@ -0,0 +1,12 @@ +indexes: + +# AUTOGENERATED + +# This index.yaml is automatically updated whenever the dev_appserver +# detects that a new type of query is run. If you want to manage the +# index.yaml file manually, remove the above marker line (the line +# saying "# AUTOGENERATED"). If you want to manage some indexes +# manually, move them above the marker line. The index.yaml file is +# automatically uploaded to the admin console when you next deploy +# your application using appcfg.py. + diff --git a/scripts/app_engine_server/memcache_zipserve.py b/scripts/app_engine_server/memcache_zipserve.py new file mode 100644 index 0000000..e11cfc5 --- /dev/null +++ b/scripts/app_engine_server/memcache_zipserve.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# Copyright 2009 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""A class to serve pages from zip files and use memcache for performance. + +This contains a class and a function to create an anonymous instance of the +class to serve HTTP GET requests. Memcache is used to increase response speed +and lower processing cycles used in serving. Credit to Guido van Rossum and +his implementation of zipserve which served as a reference as I wrote this. + + MemcachedZipHandler: Class that serves request + create_handler: method to create instance of MemcachedZipHandler +""" + +__author__ = 'jmatt@google.com (Justin Mattson)' + +import email.Utils +import logging +import mimetypes +import time +import zipfile + +from google.appengine.api import memcache +from google.appengine.ext import webapp +from google.appengine.ext.webapp import util + + +def create_handler(zip_files, max_age=None, public=None): + """Factory method to create a MemcachedZipHandler instance. + + Args: + zip_files: A list of file names, or a list of lists of file name, first + member of file mappings. See MemcachedZipHandler documentation for + more information about using the list of lists format + max_age: The maximum client-side cache lifetime + public: Whether this should be declared public in the client-side cache + Returns: + A MemcachedZipHandler wrapped in a pretty, anonymous bow for use with App + Engine + + Raises: + ValueError: if the zip_files argument is not a list + """ + # verify argument integrity. If the argument is passed in list format, + # convert it to list of lists format + + if zip_files and type(zip_files).__name__ == 'list': + num_items = len(zip_files) + while num_items > 0: + if type(zip_files[num_items - 1]).__name__ != 'list': + zip_files[num_items - 1] = [zip_files[num_items-1]] + num_items -= 1 + else: + raise ValueError('File name arguments must be a list') + + class HandlerWrapper(MemcachedZipHandler): + """Simple wrapper for an instance of MemcachedZipHandler. + + I'm still not sure why this is needed + """ + + def get(self, name): + self.zipfilenames = zip_files + self.TrueGet(name) + if max_age is not None: + MAX_AGE = max_age + if public is not None: + PUBLIC = public + + return HandlerWrapper + + +class MemcachedZipHandler(webapp.RequestHandler): + """Handles get requests for a given URL. + + Serves a GET request from a series of zip files. As files are served they are + put into memcache, which is much faster than retreiving them from the zip + source file again. It also uses considerably fewer CPU cycles. + """ + zipfile_cache = {} # class cache of source zip files + MAX_AGE = 600 # max client-side cache lifetime + PUBLIC = True # public cache setting + CACHE_PREFIX = 'cache://' # memcache key prefix for actual URLs + NEG_CACHE_PREFIX = 'noncache://' # memcache key prefix for non-existant URL + + def TrueGet(self, name): + """The top-level entry point to serving requests. + + Called 'True' get because it does the work when called from the wrapper + class' get method + + Args: + name: URL requested + + Returns: + None + """ + name = self.PreprocessUrl(name) + + # see if we have the page in the memcache + resp_data = self.GetFromCache(name) + if resp_data is None: + logging.info('Cache miss for %s', name) + resp_data = self.GetFromNegativeCache(name) + if resp_data is None: + resp_data = self.GetFromStore(name) + + # IF we have the file, put it in the memcache + # ELSE put it in the negative cache + if resp_data is not None: + self.StoreOrUpdateInCache(name, resp_data) + else: + logging.info('Adding %s to negative cache, serving 404', name) + self.StoreInNegativeCache(name) + self.Write404Error() + return + else: + self.Write404Error() + return + + content_type, encoding = mimetypes.guess_type(name) + if content_type: + self.response.headers['Content-Type'] = content_type + self.SetCachingHeaders() + self.response.out.write(resp_data) + + def PreprocessUrl(self, name): + """Any preprocessing work on the URL when it comes it. + + Put any work related to interpretting the incoming URL here. For example, + this is used to redirect requests for a directory to the index.html file + in that directory. Subclasses should override this method to do different + preprocessing. + + Args: + name: The incoming URL + + Returns: + The processed URL + """ + # handle special case of requesting the domain itself + if not name: + name = 'index.html' + + # determine if this is a request for a directory + final_path_segment = name + final_slash_offset = name.rfind('/') + if final_slash_offset != len(name) - 1: + final_path_segment = name[final_slash_offset + 1:] + if final_path_segment.find('.') == -1: + name = ''.join([name, '/']) + + # if this is a directory, redirect to index.html + if name[len(name) - 1:] == '/': + return '%s%s' % (name, 'index.html') + else: + return name + + def GetFromStore(self, file_path): + """Retrieve file from zip files. + + Get the file from the source, it must not have been in the memcache. If + possible, we'll use the zip file index to quickly locate where the file + should be found. (See MapToFileArchive documentation for assumptions about + file ordering.) If we don't have an index or don't find the file where the + index says we should, look through all the zip files to find it. + + Args: + file_path: the file that we're looking for + + Returns: + The contents of the requested file + """ + resp_data = None + file_itr = iter(self.zipfilenames) + + # check the index, if we have one, to see what archive the file is in + archive_name = self.MapFileToArchive(file_path) + if not archive_name: + archive_name = file_itr.next()[0] + + while resp_data is None and archive_name: + zip_archive = self.LoadZipFile(archive_name) + if zip_archive: + + # we expect some lookups will fail, and that's okay, 404s will deal + # with that + try: + resp_data = zip_archive.read(file_path) + except (KeyError, RuntimeError), err: + # no op + x = False + if resp_data is not None: + logging.info('%s read from %s', file_path, archive_name) + + try: + archive_name = file_itr.next()[0] + except (StopIteration), err: + archive_name = False + + return resp_data + + def LoadZipFile(self, zipfilename): + """Convenience method to load zip file. + + Just a convenience method to load the zip file from the data store. This is + useful if we ever want to change data stores and also as a means of + dependency injection for testing. This method will look at our file cache + first, and then load and cache the file if there's a cache miss + + Args: + zipfilename: the name of the zip file to load + + Returns: + The zip file requested, or None if there is an I/O error + """ + zip_archive = None + zip_archive = self.zipfile_cache.get(zipfilename) + if zip_archive is None: + try: + zip_archive = zipfile.ZipFile(zipfilename) + self.zipfile_cache[zipfilename] = zip_archive + except (IOError, RuntimeError), err: + logging.error('Can\'t open zipfile %s, cause: %s' % (zipfilename, + err)) + return zip_archive + + def MapFileToArchive(self, file_path): + """Given a file name, determine what archive it should be in. + + This method makes two critical assumptions. + (1) The zip files passed as an argument to the handler, if concatenated + in that same order, would result in a total ordering + of all the files. See (2) for ordering type. + (2) Upper case letters before lower case letters. The traversal of a + directory tree is depth first. A parent directory's files are added + before the files of any child directories + + Args: + file_path: the file to be mapped to an archive + + Returns: + The name of the archive where we expect the file to be + """ + num_archives = len(self.zipfilenames) + while num_archives > 0: + target = self.zipfilenames[num_archives - 1] + if len(target) > 1: + if self.CompareFilenames(target[1], file_path) >= 0: + return target[0] + num_archives -= 1 + + return None + + def CompareFilenames(self, file1, file2): + """Determines whether file1 is lexigraphically 'before' file2. + + WARNING: This method assumes that paths are output in a depth-first, + with parent directories' files stored before childs' + + We say that file1 is lexigraphically before file2 if the last non-matching + path segment of file1 is alphabetically before file2. + + Args: + file1: the first file path + file2: the second file path + + Returns: + A positive number if file1 is before file2 + A negative number if file2 is before file1 + 0 if filenames are the same + """ + f1_segments = file1.split('/') + f2_segments = file2.split('/') + + segment_ptr = 0 + while (segment_ptr < len(f1_segments) and + segment_ptr < len(f2_segments) and + f1_segments[segment_ptr] == f2_segments[segment_ptr]): + segment_ptr += 1 + + if len(f1_segments) == len(f2_segments): + + # we fell off the end, the paths much be the same + if segment_ptr == len(f1_segments): + return 0 + + # we didn't fall of the end, compare the segments where they differ + if f1_segments[segment_ptr] < f2_segments[segment_ptr]: + return 1 + elif f1_segments[segment_ptr] > f2_segments[segment_ptr]: + return -1 + else: + return 0 + + # the number of segments differs, we either mismatched comparing + # directories, or comparing a file to a directory + else: + + # IF we were looking at the last segment of one of the paths, + # the one with fewer segments is first because files come before + # directories + # ELSE we just need to compare directory names + if (segment_ptr + 1 == len(f1_segments) or + segment_ptr + 1 == len(f2_segments)): + return len(f2_segments) - len(f1_segments) + else: + if f1_segments[segment_ptr] < f2_segments[segment_ptr]: + return 1 + elif f1_segments[segment_ptr] > f2_segments[segment_ptr]: + return -1 + else: + return 0 + + def SetCachingHeaders(self): + """Set caching headers for the request.""" + max_age = self.MAX_AGE + self.response.headers['Expires'] = email.Utils.formatdate( + time.time() + max_age, usegmt=True) + cache_control = [] + if self.PUBLIC: + cache_control.append('public') + cache_control.append('max-age=%d' % max_age) + self.response.headers['Cache-Control'] = ', '.join(cache_control) + + def GetFromCache(self, filename): + """Get file from memcache, if available. + + Args: + filename: The URL of the file to return + + Returns: + The content of the file + """ + return memcache.get('%s%s' % (self.CACHE_PREFIX, filename)) + + def StoreOrUpdateInCache(self, filename, data): + """Store data in the cache. + + Store a piece of data in the memcache. Memcache has a maximum item size of + 1*10^6 bytes. If the data is too large, fail, but log the failure. Future + work will consider compressing the data before storing or chunking it + + Args: + filename: the name of the file to store + data: the data of the file + + Returns: + None + """ + try: + if not memcache.add('%s%s' % (self.CACHE_PREFIX, filename), data): + memcache.replace('%s%s' % (self.CACHE_PREFIX, filename), data) + except (ValueError), err: + logging.warning('Data size too large to cache\n%s' % err) + + def Write404Error(self): + """Ouptut a simple 404 response.""" + self.error(404) + self.response.out.write( + ''.join(['<html><head><title>404: Not Found</title></head>', + '<body><b><h2>Error 404</h2><br/>', + 'File not found</b></body></html>'])) + + def StoreInNegativeCache(self, filename): + """If a non-existant URL is accessed, cache this result as well. + + Future work should consider setting a maximum negative cache size to + prevent it from from negatively impacting the real cache. + + Args: + filename: URL to add ot negative cache + + Returns: + None + """ + memcache.add('%s%s' % (self.NEG_CACHE_PREFIX, filename), -1) + + def GetFromNegativeCache(self, filename): + """Retrieve from negative cache. + + Args: + filename: URL to retreive + + Returns: + The file contents if present in the negative cache. + """ + return memcache.get('%s%s' % (self.NEG_CACHE_PREFIX, filename)) + + +def main(): + application = webapp.WSGIApplication([('/([^/]+)/(.*)', + MemcachedZipHandler)]) + util.run_wsgi_app(application) + + +if __name__ == '__main__': + main() |