thirdparty/google_appengine/google/appengine/api/appinfo.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     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
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    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.
       
    16 #
       
    17 
       
    18 """AppInfo tools
       
    19 
       
    20 Library for working with AppInfo records in memory, store and load from
       
    21 configuration files.
       
    22 """
       
    23 
       
    24 
       
    25 
       
    26 
       
    27 
       
    28 import re
       
    29 
       
    30 from google.appengine.api import appinfo_errors
       
    31 from google.appengine.api import validation
       
    32 from google.appengine.api import yaml_listener
       
    33 from google.appengine.api import yaml_builder
       
    34 from google.appengine.api import yaml_object
       
    35 
       
    36 
       
    37 _URL_REGEX = r'(?!\^)/|\.|(\(.).*(?!\$).'
       
    38 _FILES_REGEX = r'(?!\^).*(?!\$).'
       
    39 
       
    40 _DELTA_REGEX = r'([1-9][0-9]*)([DdHhMm]|[sS]?)'
       
    41 _EXPIRATION_REGEX = r'\s*(%s)(\s+%s)*\s*' % (_DELTA_REGEX, _DELTA_REGEX)
       
    42 
       
    43 APP_ID_MAX_LEN = 100
       
    44 MAJOR_VERSION_ID_MAX_LEN = 100
       
    45 MAX_URL_MAPS = 100
       
    46 
       
    47 APPLICATION_RE_STRING = r'(?!-)[a-z\d\-]{1,%d}' % APP_ID_MAX_LEN
       
    48 VERSION_RE_STRING = r'(?!-)[a-z\d\-]{1,%d}' % MAJOR_VERSION_ID_MAX_LEN
       
    49 
       
    50 HANDLER_STATIC_FILES = 'static_files'
       
    51 HANDLER_STATIC_DIR = 'static_dir'
       
    52 HANDLER_SCRIPT = 'script'
       
    53 
       
    54 LOGIN_OPTIONAL = 'optional'
       
    55 LOGIN_REQUIRED = 'required'
       
    56 LOGIN_ADMIN = 'admin'
       
    57 
       
    58 RUNTIME_PYTHON = 'python'
       
    59 
       
    60 DEFAULT_SKIP_FILES = (r"^(.*/)?("
       
    61                       r"(app\.yaml)|"
       
    62                       r"(app\.yml)|"
       
    63                       r"(index\.yaml)|"
       
    64                       r"(index\.yml)|"
       
    65                       r"(#.*#)|"
       
    66                       r"(.*~)|"
       
    67                       r"(.*\.py[co])|"
       
    68                       r"(.*/RCS/.*)|"
       
    69                       r"(\..*)|"
       
    70                       r")$")
       
    71 
       
    72 LOGIN = 'login'
       
    73 URL = 'url'
       
    74 STATIC_FILES = 'static_files'
       
    75 UPLOAD = 'upload'
       
    76 STATIC_DIR = 'static_dir'
       
    77 MIME_TYPE = 'mime_type'
       
    78 SCRIPT = 'script'
       
    79 EXPIRATION = 'expiration'
       
    80 
       
    81 APPLICATION = 'application'
       
    82 VERSION = 'version'
       
    83 RUNTIME = 'runtime'
       
    84 API_VERSION = 'api_version'
       
    85 HANDLERS = 'handlers'
       
    86 DEFAULT_EXPIRATION = 'default_expiration'
       
    87 SKIP_FILES = 'skip_files'
       
    88 
       
    89 
       
    90 class URLMap(validation.Validated):
       
    91   """Mapping from URLs to handlers.
       
    92 
       
    93   This class acts like something of a union type.  Its purpose is to
       
    94   describe a mapping between a set of URLs and their handlers.  What
       
    95   handler type a given instance has is determined by which handler-id
       
    96   attribute is used.
       
    97 
       
    98   Each mapping can have one and only one handler type.  Attempting to
       
    99   use more than one handler-id attribute will cause an UnknownHandlerType
       
   100   to be raised during validation.  Failure to provide any handler-id
       
   101   attributes will cause MissingHandlerType to be raised during validation.
       
   102 
       
   103   The regular expression used by the url field will be used to match against
       
   104   the entire URL path and query string of the request.  This means that
       
   105   partial maps will not be matched.  Specifying a url, say /admin, is the
       
   106   same as matching against the regular expression '^/admin$'.  Don't begin
       
   107   your matching url with ^ or end them with $.  These regular expressions
       
   108   won't be accepted and will raise ValueError.
       
   109 
       
   110   Attributes:
       
   111     login: Whether or not login is required to access URL.  Defaults to
       
   112       'optional'.
       
   113     url: Regular expression used to fully match against the request URLs path.
       
   114       See Special Cases for using static_dir.
       
   115     static_files: Handler id attribute that maps URL to the appropriate
       
   116       file.  Can use back regex references to the string matched to url.
       
   117     upload: Regular expression used by the application configuration
       
   118       program to know which files are uploaded as blobs.  It's very
       
   119       difficult to determine this using just the url and static_files
       
   120       so this attribute must be included.  Required when defining a
       
   121       static_files mapping.
       
   122       A matching file name must fully match against the upload regex, similar
       
   123       to how url is matched against the request path.  Do not begin upload
       
   124       with ^ or end it with $.
       
   125     static_dir: Handler id that maps the provided url to a sub-directory
       
   126       within the application directory.  See Special Cases.
       
   127     mime_type: When used with static_files and static_dir the mime-type
       
   128       of files served from those directories are overridden with this
       
   129       value.
       
   130     script: Handler id that maps URLs to scipt handler within the application
       
   131       directory that will run using CGI.
       
   132     expiration: When used with static files and directories, the time delta to
       
   133       use for cache expiration. Has the form '4d 5h 30m 15s', where each letter
       
   134       signifies days, hours, minutes, and seconds, respectively. The 's' for
       
   135       seconds may be omitted. Only one amount must be specified, combining
       
   136       multiple amounts is optional. Example good values: '10', '1d 6h',
       
   137       '1h 30m', '7d 7d 7d', '5m 30'.
       
   138 
       
   139   Special cases:
       
   140     When defining a static_dir handler, do not use a regular expression
       
   141     in the url attribute.  Both the url and static_dir attributes are
       
   142     automatically mapped to these equivalents:
       
   143 
       
   144       <url>/(.*)
       
   145       <static_dir>/\1
       
   146 
       
   147     For example:
       
   148 
       
   149       url: /images
       
   150       static_dir: images_folder
       
   151 
       
   152     Is the same as this static_files declaration:
       
   153 
       
   154       url: /images/(.*)
       
   155       static_files: images/\1
       
   156       upload: images/(.*)
       
   157   """
       
   158 
       
   159   ATTRIBUTES = {
       
   160 
       
   161     URL: validation.Optional(_URL_REGEX),
       
   162     LOGIN: validation.Options(LOGIN_OPTIONAL,
       
   163                               LOGIN_REQUIRED,
       
   164                               LOGIN_ADMIN,
       
   165                               default=LOGIN_OPTIONAL),
       
   166 
       
   167 
       
   168 
       
   169     HANDLER_STATIC_FILES: validation.Optional(_FILES_REGEX),
       
   170     UPLOAD: validation.Optional(_FILES_REGEX),
       
   171 
       
   172 
       
   173     HANDLER_STATIC_DIR: validation.Optional(_FILES_REGEX),
       
   174 
       
   175 
       
   176     MIME_TYPE: validation.Optional(str),
       
   177     EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
       
   178 
       
   179 
       
   180     HANDLER_SCRIPT: validation.Optional(_FILES_REGEX),
       
   181   }
       
   182 
       
   183   COMMON_FIELDS = set([URL, LOGIN])
       
   184 
       
   185   ALLOWED_FIELDS = {
       
   186     HANDLER_STATIC_FILES: (MIME_TYPE, UPLOAD, EXPIRATION),
       
   187     HANDLER_STATIC_DIR: (MIME_TYPE, EXPIRATION),
       
   188     HANDLER_SCRIPT: (),
       
   189   }
       
   190 
       
   191   def GetHandler(self):
       
   192     """Get handler for mapping.
       
   193 
       
   194     Returns:
       
   195       Value of the handler (determined by handler id attribute).
       
   196     """
       
   197     return getattr(self, self.GetHandlerType())
       
   198 
       
   199   def GetHandlerType(self):
       
   200     """Get handler type of mapping.
       
   201 
       
   202     Returns:
       
   203       Handler type determined by which handler id attribute is set.
       
   204 
       
   205     Raises:
       
   206       UnknownHandlerType when none of the no handler id attributes
       
   207       are set.
       
   208 
       
   209       UnexpectedHandlerAttribute when an unexpected attribute
       
   210       is set for the discovered handler type.
       
   211 
       
   212       HandlerTypeMissingAttribute when the handler is missing a
       
   213       required attribute for its handler type.
       
   214     """
       
   215     for id_field in URLMap.ALLOWED_FIELDS.iterkeys():
       
   216       if getattr(self, id_field) is not None:
       
   217         mapping_type = id_field
       
   218         break
       
   219     else:
       
   220       raise appinfo_errors.UnknownHandlerType(
       
   221           'Unknown url handler type.\n%s' % str(self))
       
   222 
       
   223     allowed_fields = URLMap.ALLOWED_FIELDS[mapping_type]
       
   224 
       
   225     for attribute in self.ATTRIBUTES.iterkeys():
       
   226       if (getattr(self, attribute) is not None and
       
   227           not (attribute in allowed_fields or
       
   228                attribute in URLMap.COMMON_FIELDS or
       
   229                attribute == mapping_type)):
       
   230             raise appinfo_errors.UnexpectedHandlerAttribute(
       
   231                 'Unexpected attribute "%s" for mapping type %s.' %
       
   232                 (attribute, mapping_type))
       
   233 
       
   234     if mapping_type == HANDLER_STATIC_FILES and not self.upload:
       
   235       raise appinfo_errors.MissingHandlerAttribute(
       
   236           'Missing "%s" attribute for URL "%s".' % (UPLOAD, self.url))
       
   237 
       
   238     return mapping_type
       
   239 
       
   240   def CheckInitialized(self):
       
   241     """Adds additional checking to make sure handler has correct fields.
       
   242 
       
   243     In addition to normal ValidatedCheck calls GetHandlerType
       
   244     which validates all the handler fields are configured
       
   245     properly.
       
   246 
       
   247     Raises:
       
   248       UnknownHandlerType when none of the no handler id attributes
       
   249       are set.
       
   250 
       
   251       UnexpectedHandlerAttribute when an unexpected attribute
       
   252       is set for the discovered handler type.
       
   253 
       
   254       HandlerTypeMissingAttribute when the handler is missing a
       
   255       required attribute for its handler type.
       
   256     """
       
   257     super(URLMap, self).CheckInitialized()
       
   258     self.GetHandlerType()
       
   259 
       
   260 
       
   261 class AppInfoExternal(validation.Validated):
       
   262   """Class representing users application info.
       
   263 
       
   264   This class is passed to a yaml_object builder to provide the validation
       
   265   for the application information file format parser.
       
   266 
       
   267   Attributes:
       
   268     application: Unique identifier for application.
       
   269     version: Application's major version number.
       
   270     runtime: Runtime used by application.
       
   271     api_version: Which version of APIs to use.
       
   272     handlers: List of URL handlers.
       
   273     default_expiration: Default time delta to use for cache expiration for
       
   274       all static files, unless they have their own specific 'expiration' set.
       
   275       See the URLMap.expiration field's documentation for more information.
       
   276     skip_files: An re object.  Files that match this regular expression will
       
   277       not be uploaded by appcfg.py.  For example:
       
   278         skip_files: |
       
   279           .svn.*|
       
   280           #.*#
       
   281   """
       
   282 
       
   283   ATTRIBUTES = {
       
   284 
       
   285 
       
   286     APPLICATION: APPLICATION_RE_STRING,
       
   287     VERSION: VERSION_RE_STRING,
       
   288     RUNTIME: validation.Options(RUNTIME_PYTHON),
       
   289 
       
   290 
       
   291     API_VERSION: validation.Options('1', 'beta'),
       
   292     HANDLERS: validation.Optional(validation.Repeated(URLMap)),
       
   293     DEFAULT_EXPIRATION: validation.Optional(_EXPIRATION_REGEX),
       
   294     SKIP_FILES: validation.RegexStr(default=DEFAULT_SKIP_FILES)
       
   295   }
       
   296 
       
   297   def CheckInitialized(self):
       
   298     """Ensures that at least one url mapping is provided.
       
   299 
       
   300     Raises:
       
   301       MissingURLMapping when no URLMap objects are present in object.
       
   302       TooManyURLMappings when there are too many URLMap entries.
       
   303     """
       
   304     super(AppInfoExternal, self).CheckInitialized()
       
   305     if not self.handlers:
       
   306       raise appinfo_errors.MissingURLMapping(
       
   307           'No URLMap entries found in application configuration')
       
   308     if len(self.handlers) > MAX_URL_MAPS:
       
   309       raise appinfo_errors.TooManyURLMappings(
       
   310           'Found more than %d URLMap entries in application configuration' %
       
   311           MAX_URL_MAPS)
       
   312 
       
   313 
       
   314 def LoadSingleAppInfo(app_info):
       
   315   """Load a single AppInfo object where one and only one is expected.
       
   316 
       
   317   Args:
       
   318     app_info: A file-like object or string.  If it is a string, parse it as
       
   319     a configuration file.  If it is a file-like object, read in data and
       
   320     parse.
       
   321 
       
   322   Returns:
       
   323     An instance of AppInfoExternal as loaded from a YAML file.
       
   324 
       
   325   Raises:
       
   326     EmptyConfigurationFile when there are no documents in YAML file.
       
   327     MultipleConfigurationFile when there is more than one document in YAML
       
   328     file.
       
   329   """
       
   330   builder = yaml_object.ObjectBuilder(AppInfoExternal)
       
   331   handler = yaml_builder.BuilderHandler(builder)
       
   332   listener = yaml_listener.EventListener(handler)
       
   333   listener.Parse(app_info)
       
   334 
       
   335   app_infos = handler.GetResults()
       
   336   if len(app_infos) < 1:
       
   337     raise appinfo_errors.EmptyConfigurationFile()
       
   338   if len(app_infos) > 1:
       
   339     raise appinfo_errors.MultipleConfigurationFile()
       
   340   return app_infos[0]
       
   341 
       
   342 
       
   343 _file_path_positive_re = re.compile(r'^[ 0-9a-zA-Z\._\+/\$-]{1,256}$')
       
   344 
       
   345 _file_path_negative_1_re = re.compile(r'\.\.|^\./|\.$|/\./|^-')
       
   346 
       
   347 _file_path_negative_2_re = re.compile(r'//|/$')
       
   348 
       
   349 _file_path_negative_3_re = re.compile(r'^ | $|/ | /')
       
   350 
       
   351 
       
   352 def ValidFilename(filename):
       
   353   """Determines if filename is valid.
       
   354 
       
   355   filename must be a valid pathname.
       
   356   - It must contain only letters, numbers, _, +, /, $, ., and -.
       
   357   - It must be less than 256 chars.
       
   358   - It must not contain "/./", "/../", or "//".
       
   359   - It must not end in "/".
       
   360   - All spaces must be in the middle of a directory or file name.
       
   361 
       
   362   Args:
       
   363     filename: The filename to validate.
       
   364 
       
   365   Returns:
       
   366     An error string if the filename is invalid.  Returns '' if the filename
       
   367     is valid.
       
   368   """
       
   369   if _file_path_positive_re.match(filename) is None:
       
   370     return 'Invalid character in filename: %s' % filename
       
   371   if _file_path_negative_1_re.search(filename) is not None:
       
   372     return ('Filename cannot contain "." or ".." or start with "-": %s' %
       
   373             filename)
       
   374   if _file_path_negative_2_re.search(filename) is not None:
       
   375     return 'Filename cannot have trailing / or contain //: %s' % filename
       
   376   if _file_path_negative_3_re.search(filename) is not None:
       
   377     return 'Any spaces must be in the middle of a filename: %s' % filename
       
   378   return ''