/development/scripts/app_engine_server/memcache_zipserve.py
Python | 756 lines | 691 code | 6 blank | 59 comment | 5 complexity | 267ba30da6683191ffc65217c24f1416 MD5 | raw file
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- #
- # 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 re
- import sys
- import time
- import yaml
- import zipfile
- from google.appengine.api import memcache
- from google.appengine.ext import webapp
- from google.appengine.ext.webapp import util
- from time import localtime, strftime
- 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 = 43200 # max client-side cache lifetime, in seconds
- 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
- REDIRECT_PREFIX = 'redirect://' # memcache key prefix for redirect data
- REDIRECT_FILE = 'redirects.yaml' # Name of file that contains redirect table
- REDIRECT_SRC = 'src' # Name of the 'source' attribute for a
- # redirect table entry
- REDIRECT_DST = 'dst' # Name of the 'destination' attribute for
- # a redirect table entry
- REDIRECT_TYPE = 'type' # Name of the 'type' attribute for a
- # redirect table entry
- REDIRECT_TYPE_PERM = 'permanent' # Redirect 'type' string indicating a 301
- # redirect should be served
- REDIRECT_TYPE_TEMP = 'temporary' # Redirect 'type'string indicate a 302
- # Redirect should be served
- intlString = 'intl/'
- validLangs = ['en', 'de', 'es', 'fr','it','ja','ko','ru','zh-CN','zh-cn','zh-TW','zh-tw']
- def TrueGet(self, reqUri):
- """The top-level entry point to serving requests.
- Called 'True' get because it does the work when called from the wrapper
- class' get method. Some logic is applied to the request to serve files
- from an intl/<lang>/... directory or fall through to the default language.
- Args:
- name: URL requested
- Returns:
- None
- """
- langName = 'en'
- resetLangCookie = False
- urlLangName = None
- retry = False
- isValidIntl = False
- isStripped = False
- # Try to retrieve the user's lang pref from the cookie. If there is no
- # lang pref cookie in the request, add set-cookie to the response with the
- # default value of 'en'.
- try:
- langName = self.request.cookies['android_developer_pref_lang']
- except KeyError:
- resetLangCookie = True
- #logging.info('==========================EXCEPTION: NO LANG COOKIE FOUND, USING [%s]', langName)
- logging.info('==========================REQ INIT name [%s] langName [%s] resetLangCookie [%s]', reqUri, langName, resetLangCookie)
- # Do some prep for handling intl requests. Parse the url and validate
- # the intl/lang substring, extract the url lang code (urlLangName) and the
- # the uri that follows the intl/lang substring(contentUri)
- sections = reqUri.split("/", 2)
- isIntl = len(sections) > 2 and (sections[0] == "intl")
- if isIntl:
- isValidIntl = sections[1] in self.validLangs
- urlLangName = sections[1]
- contentUri = sections[2]
- logging.info(' Content URI is [%s]...', contentUri)
- if isValidIntl:
- if (langName != urlLangName) or (langName == 'en'):
- # if the lang code in the request is different from that in
- # the cookie, or if the target lang is en, strip the
- # intl/nn substring. It will later be redirected to
- # the user's preferred language url.
- # logging.info(' Handling a MISMATCHED intl request')
- reqUri = contentUri
- isStripped = True
- isValidIntl = False
- isIntl = False
- #logging.info('INTL PREP resetting langName to urlLangName [%s]', langName)
- #else:
- # logging.info('INTL PREP no need to reset langName')
- else:
- contentUri = reqUri
- # Apply manual redirects from redirects.yaml. This occurs before any
- # other mutations are performed, to avoid odd redirect behavior
- # (For example, a user may want to redirect a directory without having
- # /index.html appended.)
- did_redirect = self.ProcessManualRedirects(contentUri, langName, isIntl)
- if did_redirect:
- return
- # Preprocess the req url. If it references a directory or the domain itself,
- # append '/index.html' to the url and 302 redirect. Otherwise, continue
- # processing the request below.
- did_redirect = self.PreprocessUrl(reqUri, langName)
- if did_redirect:
- return
- # Send for processing
- if self.isCleanUrl(reqUri, langName, isValidIntl, isStripped):
- # handle a 'clean' request.
- # Try to form a response using the actual request url.
- # logging.info(' Request being handled as clean: [%s]', name)
- if not self.CreateResponse(reqUri, langName, isValidIntl, resetLangCookie):
- # If CreateResponse returns False, there was no such document
- # in the intl/lang tree. Before going to 404, see if there is an
- # English-language version of the doc in the default
- # default tree and return it, else go to 404.
- self.CreateResponse(contentUri, langName, False, resetLangCookie)
- elif isIntl:
- # handle the case where we need to pass through an invalid intl req
- # for processing (so as to get 404 as appropriate). This is needed
- # because intl urls are passed through clean and retried in English,
- # if necessary.
- # logging.info(' Handling an invalid intl request...')
- self.CreateResponse(reqUri, langName, isValidIntl, resetLangCookie)
- else:
- # handle the case where we have a non-clean url (usually a non-intl
- # url) that we need to interpret in the context of any lang pref
- # that is set. Prepend an intl/lang string to the request url and
- # send it as a 302 redirect. After the redirect, the subsequent
- # request will be handled as a clean url.
- self.RedirToIntl(reqUri, self.intlString, langName)
- def ProcessManualRedirects(self, contentUri, langName, isIntl):
- """Compute any manual redirects for a request and execute them.
- This allows content authors to manually define a set of regex rules which,
- when matched, will cause an HTTP redirect to be performed.
- Redirect rules are typically stored in a file named redirects.yaml. See the
- comments in that file for more information about formatting.
- Redirect computations are stored in memcache for performance.
- Note that international URIs are handled automatically, and are assumed to
- mirror redirects for non-intl requests.
- Args:
- contentUri: The relative URI (without leading slash) that was requested.
- This should NOT contain an intl-prefix, if otherwise present.
- langName: The requested language.
- isIntl: True if contentUri originally contained an intl prefix.
- Results:
- boolean: True if a redirect has been set, False otherwise.
- """
- # Redirect data is stored in memcache for performance
- memcache_key = self.REDIRECT_PREFIX + contentUri
- redirect_data = memcache.get(memcache_key)
- if redirect_data is None:
- logging.info('Redirect cache miss. Computing new redirect data.\n'
- 'Memcache Key: ' + memcache_key)
- redirect_data = self.ComputeManualRedirectUrl(contentUri)
- memcache.set(memcache_key, redirect_data)
- contentUri = redirect_data[0]
- redirectType = redirect_data[1]
- # If this is an international URL, prepend intl path to minimize
- # number of redirects
- if isIntl:
- contentUri = '/%s%s%s' % (self.intlString, langName, contentUri)
- if redirectType is None:
- # No redirect necessary
- return False
- elif redirectType == self.REDIRECT_TYPE_PERM:
- logging.info('Sending permanent redirect: ' + contentUri);
- self.redirect(contentUri, permanent=True)
- return True
- elif redirectType == self.REDIRECT_TYPE_TEMP:
- logging.info('Sending temporary redirect: ' + contentUri);
- self.redirect(contentUri, permanent=False)
- return True
- else:
- # Invalid redirect type
- logging.error('Invalid redirect type: %s', redirectType)
- raise ('Invalid redirect type: %s', redirectType)
- def ComputeManualRedirectUrl(self, uri):
- """Read redirects file and evaluate redirect rules for a given URI.
- Args:
- uri: The relative URI (without leading slash) for which redirect data
- should be computed. No special handling of intl URIs is pefromed
- at this level.
- Returns:
- tuple: The computed redirect data. This tuple has two parts:
- redirect_uri: The new URI that should be used. (If no redirect rule is
- found, the original input to 'uri' will be returned.
- redirect_type: Either 'permanent' for an HTTP 301 redirect, 'temporary'
- for an HTTP 302 redirect, or None if no redirect should be performed.
- """
- # Redircts are defined in a file named redirects.yaml.
- try:
- f = open(self.REDIRECT_FILE)
- data = yaml.load(f)
- f.close()
- except IOError, e:
- logging.warning('Error opening redirect file (' + self.REDIRECT_FILE +
- '): ' + e.strerror)
- return (uri, None)
- # The incoming path is missing a leading slash. However, many parts of the
- # redirect system require leading slashes to distinguish between relative
- # and absolute redirects. So, to compensate for this, we'll add a leading
- # slash here as well.
- uri = '/' + uri
- # Check to make sure we actually got an iterable list out of the YAML file
- if data is None:
- logging.warning('Redirect file (' + self.REDIRECT_FILE + ') not valid '
- 'YAML.')
- elif 'redirects' not in data:
- logging.warning('Redirect file (' + self.REDIRECT_FILE + ') not '
- 'properly formatted -- no \'redirects:\' header.')
- elif hasattr(data['redirects'], '__iter__'):
- # Iterate through redirect data, try to find a redirect that matches.
- for redirect in data['redirects']:
- # Note: re.search adds an implied '^' to the beginning of the regex
- # This means that the regex must match from the beginning of the
- # string.
- try:
- if re.match(redirect[self.REDIRECT_SRC], uri):
- # Match found. Apply redirect rule.
- redirect_uri = re.sub('^' + redirect[self.REDIRECT_SRC],
- redirect[self.REDIRECT_DST], uri)
- logging.info('Redirect rule matched.\n'
- 'Rule: %s\n'
- 'Src: %s\n'
- 'Dst: %s',
- redirect[self.REDIRECT_SRC], uri, redirect_uri)
- if self.REDIRECT_TYPE in redirect:
- redirect_type = redirect[self.REDIRECT_TYPE]
- else:
- # Default redirect type, if unspecified
- redirect_type = self.REDIRECT_TYPE_PERM
- return (redirect_uri, redirect_type)
- except:
- e = sys.exc_info()[1]
- raise ('Error while processing redirect rule.\n'
- 'Rule: %s\n'
- 'Error: %s' % (redirect[self.REDIRECT_SRC], e))
- # No redirect found, return URL unchanged
- return (uri, None)
- def isCleanUrl(self, name, langName, isValidIntl, isStripped):
- """Determine whether to pass an incoming url straight to processing.
- Args:
- name: The incoming URL
- Returns:
- boolean: Whether the URL should be sent straight to processing
- """
- # logging.info(' >>>> isCleanUrl name [%s] langName [%s] isValidIntl [%s]', name, langName, isValidIntl)
- if (langName == 'en' and not isStripped) or isValidIntl or not ('.html' in name) or (not isValidIntl and not langName):
- return True
- def PreprocessUrl(self, name, langName):
- """Any preprocessing work on the URL when it comes in.
- Put any work related to interpreting 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:
- True if the request was redirected to '/index.html'.
- Otherewise False.
- """
- # 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 or the domain itself, redirect to /index.html
- if not name or (name[len(name) - 1:] == '/'):
- uri = ''.join(['/', name, 'index.html'])
- # logging.info('--->PREPROCESSING REDIRECT [%s] to [%s] with langName [%s]', name, uri, langName)
- self.redirect(uri, False)
- return True
- else:
- return False
- def RedirToIntl(self, name, intlString, langName):
- """Redirect an incoming request to the appropriate intl uri.
- For non-en langName, builds the intl/lang string from a
- base (en) string and redirects (302) the request to look for
- a version of the file in langName. For en langName, simply
- redirects a stripped uri string (intl/nn removed).
- Args:
- name: The incoming, preprocessed URL
- Returns:
- The lang-specific URL
- """
- if not (langName == 'en'):
- builtIntlLangUri = ''.join([intlString, langName, '/', name, '?', self.request.query_string])
- else:
- builtIntlLangUri = name
- uri = ''.join(['/', builtIntlLangUri])
- logging.info('-->REDIRECTING %s to %s', name, uri)
- self.redirect(uri, False)
- return uri
- def CreateResponse(self, name, langName, isValidIntl, resetLangCookie):
- """Process the url and form a response, if appropriate.
- Attempts to retrieve the requested file (name) from cache,
- negative cache, or store (zip) and form the response.
- For intl requests that are not found (in the localized tree),
- returns False rather than forming a response, so that
- the request can be retried with the base url (this is the
- fallthrough to default language).
- For requests that are found, forms the headers and
- adds the content to the response entity. If the request was
- for an intl (localized) url, also resets the language cookie
- to the language specified in the url if needed, to ensure that
- the client language and response data remain harmonious.
- Args:
- name: The incoming, preprocessed URL
- langName: The language id. Used as necessary to reset the
- language cookie in the response.
- isValidIntl: If present, indicates whether the request is
- for a language-specific url
- resetLangCookie: Whether the response should reset the
- language cookie to 'langName'
- Returns:
- True: A response was successfully created for the request
- False: No response was created.
- """
- # see if we have the page in the memcache
- logging.info('PROCESSING %s langName [%s] isValidIntl [%s] resetLang [%s]',
- name, langName, isValidIntl, resetLangCookie)
- 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)
- elif isValidIntl:
- # couldn't find the intl doc. Try to fall through to English.
- #logging.info(' Retrying with base uri...')
- return False
- else:
- logging.info(' Adding %s to negative cache, serving 404', name)
- self.StoreInNegativeCache(name)
- self.Write404Error()
- return True
- else:
- # found it in negative cache
- self.Write404Error()
- return True
- # found content from cache or store
- logging.info('FOUND CLEAN')
- if resetLangCookie:
- logging.info(' Resetting android_developer_pref_lang cookie to [%s]',
- langName)
- expireDate = time.mktime(localtime()) + 60 * 60 * 24 * 365 * 10
- self.response.headers.add_header('Set-Cookie',
- 'android_developer_pref_lang=%s; path=/; expires=%s' %
- (langName, strftime("%a, %d %b %Y %H:%M:%S", localtime(expireDate))))
- mustRevalidate = False
- if ('.html' in name):
- # revalidate html files -- workaround for cache inconsistencies for
- # negotiated responses
- mustRevalidate = True
- #logging.info(' Adding [Vary: Cookie] to response...')
- self.response.headers.add_header('Vary', 'Cookie')
- content_type, encoding = mimetypes.guess_type(name)
- if content_type:
- self.response.headers['Content-Type'] = content_type
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif (name == 'favicon.ico'):
- self.response.headers['Content-Type'] = 'image/x-icon'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif name.endswith('.psd'):
- self.response.headers['Content-Type'] = 'application/octet-stream'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif name.endswith('.svg'):
- self.response.headers['Content-Type'] = 'image/svg+xml'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif name.endswith('.mp4'):
- self.response.headers['Content-Type'] = 'video/mp4'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif name.endswith('.webm'):
- self.response.headers['Content-Type'] = 'video/webm'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- elif name.endswith('.ogv'):
- self.response.headers['Content-Type'] = 'video/ogg'
- self.SetCachingHeaders(mustRevalidate)
- self.response.out.write(resp_data)
- return True
- 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)
- # decode any escape characters in the URI
- # Note: We are currenty just looking for '@' (%40)
- file_path = file_path.replace('%40', '@')
- # 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, revalidate):
- """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)
- if revalidate:
- cache_control.append('must-revalidate')
- 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()