Home | History | Annotate | Download | only in server2
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 import os
      7 import traceback
      8 
      9 from chroot_file_system import ChrootFileSystem
     10 from content_provider import ContentProvider
     11 import environment
     12 from extensions_paths import CONTENT_PROVIDERS, LOCAL_DEBUG_DIR
     13 from future import Future
     14 from gitiles_file_system import GitilesFileSystem
     15 from local_file_system import LocalFileSystem
     16 from third_party.json_schema_compiler.memoize import memoize
     17 
     18 
     19 _IGNORE_MISSING_CONTENT_PROVIDERS = [False]
     20 
     21 
     22 def IgnoreMissingContentProviders(fn):
     23   '''Decorates |fn| to ignore missing content providers during its run.
     24   '''
     25   def run(*args, **optargs):
     26     saved = _IGNORE_MISSING_CONTENT_PROVIDERS[0]
     27     _IGNORE_MISSING_CONTENT_PROVIDERS[0] = True
     28     try:
     29       return fn(*args, **optargs)
     30     finally:
     31       _IGNORE_MISSING_CONTENT_PROVIDERS[0] = saved
     32   return run
     33 
     34 
     35 class ContentProviders(object):
     36   '''Implements the content_providers.json configuration; see
     37   chrome/common/extensions/docs/templates/json/content_providers.json for its
     38   current state and a description of the format.
     39 
     40   Returns ContentProvider instances based on how they're configured there.
     41   '''
     42 
     43   def __init__(self,
     44                object_store_creator,
     45                compiled_fs_factory,
     46                host_file_system,
     47                github_file_system_provider,
     48                gcs_file_system_provider):
     49     self._object_store_creator = object_store_creator
     50     self._compiled_fs_factory = compiled_fs_factory
     51     self._host_file_system = host_file_system
     52     self._github_file_system_provider = github_file_system_provider
     53     self._gcs_file_system_provider = gcs_file_system_provider
     54     self._cache = None
     55 
     56     # If running the devserver and there is a LOCAL_DEBUG_DIR, we
     57     # will read the content_provider configuration from there instead
     58     # of fetching it from Gitiles or patch.
     59     if environment.IsDevServer() and os.path.exists(LOCAL_DEBUG_DIR):
     60       local_fs = LocalFileSystem(LOCAL_DEBUG_DIR)
     61       conf_stat = None
     62       try:
     63         conf_stat = local_fs.Stat(CONTENT_PROVIDERS)
     64       except:
     65         pass
     66 
     67       if conf_stat:
     68         logging.warn(("Using local debug folder (%s) for "
     69                       "content_provider.json configuration") % LOCAL_DEBUG_DIR)
     70         self._cache = compiled_fs_factory.ForJson(local_fs)
     71 
     72     if not self._cache:
     73       self._cache = compiled_fs_factory.ForJson(host_file_system)
     74 
     75   @memoize
     76   def GetByName(self, name):
     77     '''Gets the ContentProvider keyed by |name| in content_providers.json, or
     78     None of there is no such content provider.
     79     '''
     80     config = self._GetConfig().get(name)
     81     if config is None:
     82       logging.error('No content provider found with name "%s"' % name)
     83       return None
     84     return self._CreateContentProvider(name, config)
     85 
     86   @memoize
     87   def GetByServeFrom(self, path):
     88     '''Gets a (content_provider, serve_from, path_in_content_provider) tuple,
     89     where content_provider is the ContentProvider with the longest "serveFrom"
     90     property that is a subpath of |path|, serve_from is that property, and
     91     path_in_content_provider is the remainder of |path|.
     92 
     93     For example, if content provider A serves from "foo" and content provider B
     94     serves from "foo/bar", GetByServeFrom("foo/bar/baz") will return (B,
     95     "foo/bar", "baz").
     96 
     97     Returns (None, '', |path|) if no ContentProvider serves from |path|.
     98     '''
     99     serve_from_to_config = dict(
    100         (config['serveFrom'], (name, config))
    101         for name, config in self._GetConfig().iteritems())
    102     path_parts = path.split('/')
    103     for i in xrange(len(path_parts), -1, -1):
    104       name_and_config = serve_from_to_config.get('/'.join(path_parts[:i]))
    105       if name_and_config is not None:
    106         return (self._CreateContentProvider(name_and_config[0],
    107                                             name_and_config[1]),
    108                 '/'.join(path_parts[:i]),
    109                 '/'.join(path_parts[i:]))
    110     return None, '', path
    111 
    112   def _GetConfig(self):
    113     return self._cache.GetFromFile(CONTENT_PROVIDERS).Get()
    114 
    115   def _CreateContentProvider(self, name, config):
    116     default_extensions = config.get('defaultExtensions', ())
    117     supports_templates = config.get('supportsTemplates', False)
    118     supports_zip = config.get('supportsZip', False)
    119 
    120     if 'chromium' in config:
    121       chromium_config = config['chromium']
    122       if 'dir' not in chromium_config:
    123         logging.error('%s: "chromium" must have a "dir" property' % name)
    124         return None
    125       file_system = ChrootFileSystem(self._host_file_system,
    126 
    127                                      chromium_config['dir'])
    128     # TODO(rockot): Remove this in a future patch. It should not be needed once
    129     # the new content_providers.json is committed.
    130     elif 'gitiles' in config:
    131       chromium_config = config['gitiles']
    132       if 'dir' not in chromium_config:
    133         logging.error('%s: "chromium" must have a "dir" property' % name)
    134         return None
    135       file_system = ChrootFileSystem(self._host_file_system,
    136                                      chromium_config['dir'])
    137     elif 'gcs' in config:
    138       gcs_config = config['gcs']
    139       if 'bucket' not in gcs_config:
    140         logging.error('%s: "gcs" must have a "bucket" property' % name)
    141         return None
    142       bucket = gcs_config['bucket']
    143       if not bucket.startswith('gs://'):
    144         logging.error('%s: bucket %s should start with gs://' % (name, bucket))
    145         return None
    146       bucket = bucket[len('gs://'):]
    147       file_system = self._gcs_file_system_provider.Create(bucket)
    148       if 'dir' in gcs_config:
    149         file_system = ChrootFileSystem(file_system, gcs_config['dir'])
    150 
    151     elif 'github' in config:
    152       github_config = config['github']
    153       if 'owner' not in github_config or 'repo' not in github_config:
    154         logging.error('%s: "github" must provide an "owner" and "repo"' % name)
    155         return None
    156       file_system = self._github_file_system_provider.Create(
    157           github_config['owner'], github_config['repo'])
    158       if 'dir' in github_config:
    159         file_system = ChrootFileSystem(file_system, github_config['dir'])
    160 
    161     else:
    162       logging.error('%s: content provider type not supported' % name)
    163       return None
    164 
    165     return ContentProvider(name,
    166                            self._compiled_fs_factory,
    167                            file_system,
    168                            self._object_store_creator,
    169                            default_extensions=default_extensions,
    170                            supports_templates=supports_templates,
    171                            supports_zip=supports_zip)
    172 
    173   def GetRefreshPaths(self):
    174     return self._GetConfig().keys()
    175 
    176   def Refresh(self, path):
    177     def safe(name, action, callback):
    178       '''Safely runs |callback| for a ContentProvider called |name| by
    179       swallowing exceptions and turning them into a None return value. It's
    180       important to run all ContentProvider Refreshes even if some of them fail.
    181       '''
    182       try:
    183         return callback()
    184       except:
    185         if not _IGNORE_MISSING_CONTENT_PROVIDERS[0]:
    186           logging.error('Error %s Refresh for ContentProvider "%s":\n%s' %
    187                         (action, name, traceback.format_exc()))
    188         return None
    189 
    190     config = self._GetConfig()[path]
    191     provider = self._CreateContentProvider(path, config)
    192     future = safe(path,
    193                   'initializing',
    194                   self._CreateContentProvider(path, config).Refresh)
    195     if future is None:
    196       return Future(callback=lambda: True)
    197     return Future(callback=lambda: safe(path, 'resolving', future.Get))
    198