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