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 gitiles_file_system import GitilesFileSystem
15from local_file_system import LocalFileSystem
16from third_party.json_schema_compiler.memoize import memoize
17
18
19_IGNORE_MISSING_CONTENT_PROVIDERS = [False]
20
21
22def 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
35class 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