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