| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 | 5 |
| 6 from base64 import b64decode | 6 from base64 import b64decode |
| 7 from itertools import izip | 7 from itertools import izip |
| 8 import json |
| 8 import logging | 9 import logging |
| 9 import json | |
| 10 import posixpath | 10 import posixpath |
| 11 import time | 11 import time |
| 12 import traceback | 12 import traceback |
| 13 | 13 |
| 14 from appengine_url_fetcher import AppEngineUrlFetcher | 14 from appengine_url_fetcher import AppEngineUrlFetcher |
| 15 from appengine_wrappers import IsDownloadError, app_identity | 15 from appengine_wrappers import IsDownloadError, app_identity |
| 16 from docs_server_utils import StringIdentity | 16 from docs_server_utils import StringIdentity |
| 17 from file_system import (FileNotFoundError, | 17 from file_system import (FileNotFoundError, |
| 18 FileSystem, | 18 FileSystem, |
| 19 FileSystemError, | 19 FileSystemError, |
| 20 FileSystemThrottledError, |
| 20 StatInfo) | 21 StatInfo) |
| 21 from future import All, Future | 22 from future import All, Future |
| 22 from path_util import AssertIsValid, IsDirectory, ToDirectory | 23 from path_util import AssertIsValid, IsDirectory, ToDirectory |
| 23 from third_party.json_schema_compiler.memoize import memoize | 24 from third_party.json_schema_compiler.memoize import memoize |
| 24 from url_constants import (GITILES_BASE, | 25 from url_constants import (GITILES_BASE, |
| 25 GITILES_BRANCH_BASE, | 26 GITILES_SRC_ROOT, |
| 27 GITILES_BRANCHES_PATH, |
| 26 GITILES_OAUTH2_SCOPE) | 28 GITILES_OAUTH2_SCOPE) |
| 27 | 29 |
| 30 |
| 28 _JSON_FORMAT = '?format=JSON' | 31 _JSON_FORMAT = '?format=JSON' |
| 29 _TEXT_FORMAT = '?format=TEXT' | 32 _TEXT_FORMAT = '?format=TEXT' |
| 33 _AUTH_PATH_PREFIX = '/a' |
| 30 | 34 |
| 31 | 35 |
| 32 def _ParseGitilesJson(json_data): | 36 def _ParseGitilesJson(json_data): |
| 33 '''json.loads with fix-up for non-executable JSON. Use this to parse any JSON | 37 '''json.loads with fix-up for non-executable JSON. Use this to parse any JSON |
| 34 data coming from Gitiles views. | 38 data coming from Gitiles views. |
| 35 ''' | 39 ''' |
| 36 return json.loads(json_data[json_data.find('{'):]) | 40 return json.loads(json_data[json_data.find('{'):]) |
| 37 | 41 |
| 38 | 42 |
| 39 def _CreateStatInfo(json_data): | 43 def _CreateStatInfo(json_data): |
| 40 '''Returns a StatInfo object comprised of the tree ID for |json_data|, | 44 '''Returns a StatInfo object comprised of the tree ID for |json_data|, |
| 41 as well as the tree IDs for the entries in |json_data|. | 45 as well as the tree IDs for the entries in |json_data|. |
| 42 ''' | 46 ''' |
| 43 tree = _ParseGitilesJson(json_data) | 47 tree = _ParseGitilesJson(json_data) |
| 44 return StatInfo(tree['id'], | 48 return StatInfo(tree['id'], |
| 45 dict((e['name'], e['id']) for e in tree['entries'])) | 49 dict((e['name'], e['id']) for e in tree['entries'])) |
| 46 | 50 |
| 47 | 51 |
| 48 class GitilesFileSystem(FileSystem): | 52 class GitilesFileSystem(FileSystem): |
| 49 '''Class to fetch filesystem data from the Chromium project's gitiles | 53 '''Class to fetch filesystem data from the Chromium project's gitiles |
| 50 service. | 54 service. |
| 51 ''' | 55 ''' |
| 52 @staticmethod | 56 @staticmethod |
| 53 def Create(branch='master', commit=None): | 57 def Create(branch='master', commit=None): |
| 58 token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE) |
| 59 path_prefix = '' if token is None else _AUTH_PATH_PREFIX |
| 54 if commit: | 60 if commit: |
| 55 base_url = '%s/%s' % (GITILES_BASE, commit) | 61 base_url = '%s%s/%s/%s' % ( |
| 62 GITILES_BASE, path_prefix, GITILES_SRC_ROOT, commit) |
| 56 elif branch is 'master': | 63 elif branch is 'master': |
| 57 base_url = '%s/master' % GITILES_BASE | 64 base_url = '%s%s/%s/master' % ( |
| 65 GITILES_BASE, path_prefix, GITILES_SRC_ROOT) |
| 58 else: | 66 else: |
| 59 base_url = '%s/%s' % (GITILES_BRANCH_BASE, branch) | 67 base_url = '%s%s/%s/%s/%s' % ( |
| 68 GITILES_BASE, path_prefix, GITILES_SRC_ROOT, |
| 69 GITILES_BRANCHES_PATH, branch) |
| 60 return GitilesFileSystem(AppEngineUrlFetcher(), base_url, branch, commit) | 70 return GitilesFileSystem(AppEngineUrlFetcher(), base_url, branch, commit) |
| 61 | 71 |
| 62 def __init__(self, fetcher, base_url, branch, commit): | 72 def __init__(self, fetcher, base_url, branch, commit): |
| 63 self._fetcher = fetcher | 73 self._fetcher = fetcher |
| 64 self._base_url = base_url | 74 self._base_url = base_url |
| 65 self._branch = branch | 75 self._branch = branch |
| 66 self._commit = commit | 76 self._commit = commit |
| 67 | 77 |
| 68 def _FetchAsync(self, url): | 78 def _FetchAsync(self, url): |
| 69 '''Convenience wrapper for fetcher.FetchAsync, so callers don't | 79 '''Convenience wrapper for fetcher.FetchAsync, so callers don't |
| 70 need to use posixpath.join. | 80 need to use posixpath.join. |
| 71 ''' | 81 ''' |
| 72 AssertIsValid(url) | 82 AssertIsValid(url) |
| 73 access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE) | 83 access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE) |
| 74 return self._fetcher.FetchAsync('%s/%s' % (self._base_url, url), | 84 return self._fetcher.FetchAsync('%s/%s' % (self._base_url, url), |
| 75 access_token=access_token) | 85 access_token=access_token) |
| 76 | 86 |
| 77 def _ResolveFetchContent(self, path, fetch_future, retry, | 87 def _ResolveFetchContent(self, path, fetch_future, skip_not_found=False): |
| 78 skip_not_found=False): | |
| 79 '''Returns a future to cleanly resolve |fetch_future|. | 88 '''Returns a future to cleanly resolve |fetch_future|. |
| 80 ''' | 89 ''' |
| 81 def handle(e): | 90 def handle(e): |
| 82 if skip_not_found and IsDownloadError(e): | 91 if skip_not_found and IsDownloadError(e): |
| 83 return None | 92 return None |
| 84 exc_type = FileNotFoundError if IsDownloadError(e) else FileSystemError | 93 exc_type = FileNotFoundError if IsDownloadError(e) else FileSystemError |
| 85 raise exc_type('%s fetching %s for Get from %s: %s' % | 94 raise exc_type('%s fetching %s for Get from %s: %s' % |
| 86 (type(e).__name__, path, self._base_url, traceback.format_exc())) | 95 (type(e).__name__, path, self._base_url, traceback.format_exc())) |
| 87 | 96 |
| 88 def get_content(result): | 97 def get_content(result): |
| 89 if result.status_code == 404: | 98 if result.status_code == 404: |
| 90 if skip_not_found: | 99 if skip_not_found: |
| 91 return None | 100 return None |
| 92 raise FileNotFoundError('Got 404 when fetching %s for Get from %s' % | 101 raise FileNotFoundError('Got 404 when fetching %s for Get from %s' % |
| 93 (path, self._base_url)) | 102 (path, self._base_url)) |
| 94 if result.status_code == 429: | 103 if result.status_code == 429: |
| 95 logging.warning('Access throttled when fetching %s for Get from %s' % | 104 logging.warning('Access throttled when fetching %s for Get from %s' % |
| 96 (path, self._base_url)) | 105 (path, self._base_url)) |
| 97 time.sleep(30) | 106 raise FileSystemThrottledError( |
| 98 return retry().Then(get_content, handle) | 107 'Access throttled when fetching %s for Get from %s' % |
| 108 (path, self._base_url)) |
| 99 if result.status_code != 200: | 109 if result.status_code != 200: |
| 100 raise FileSystemError( | 110 raise FileSystemError( |
| 101 'Got %s when fetching %s for Get from %s, content %s' % | 111 'Got %s when fetching %s for Get from %s, content %s' % |
| 102 (result.status_code, path, self._base_url, result.content)) | 112 (result.status_code, path, self._base_url, result.content)) |
| 103 return result.content | 113 return result.content |
| 104 | 114 |
| 105 return fetch_future.Then(get_content, handle) | 115 return fetch_future.Then(get_content, handle) |
| 106 | 116 |
| 107 def Read(self, paths, skip_not_found=False): | 117 def Read(self, paths, skip_not_found=False): |
| 108 # Directory content is formatted in JSON in Gitiles as follows: | 118 # Directory content is formatted in JSON in Gitiles as follows: |
| (...skipping 15 matching lines...) Expand all Loading... |
| 124 return [e['name'] + ('/' if e['type'] == 'tree' else '') for e in entries] | 134 return [e['name'] + ('/' if e['type'] == 'tree' else '') for e in entries] |
| 125 | 135 |
| 126 def fixup_url_format(path): | 136 def fixup_url_format(path): |
| 127 # By default, Gitiles URLs display resources in HTML. To get resources | 137 # By default, Gitiles URLs display resources in HTML. To get resources |
| 128 # suitable for our consumption, a '?format=' string must be appended to | 138 # suitable for our consumption, a '?format=' string must be appended to |
| 129 # the URL. The format may be one of 'JSON' or 'TEXT' for directory or | 139 # the URL. The format may be one of 'JSON' or 'TEXT' for directory or |
| 130 # text resources, respectively. | 140 # text resources, respectively. |
| 131 return path + (_JSON_FORMAT if IsDirectory(path) else _TEXT_FORMAT) | 141 return path + (_JSON_FORMAT if IsDirectory(path) else _TEXT_FORMAT) |
| 132 | 142 |
| 133 # A list of tuples of the form (path, Future). | 143 # A list of tuples of the form (path, Future). |
| 134 fetches = [] | 144 fetches = [(path, self._FetchAsync(fixup_url_format(path))) |
| 135 for path in paths: | 145 for path in paths] |
| 136 def make_fetch_future(): | |
| 137 return self._FetchAsync(fixup_url_format(path)) | |
| 138 fetches.append((path, make_fetch_future(), make_fetch_future)) | |
| 139 | 146 |
| 140 def parse_contents(results): | 147 def parse_contents(results): |
| 141 value = {} | 148 value = {} |
| 142 for path, content in izip(paths, results): | 149 for path, content in izip(paths, results): |
| 143 if content is None: | 150 if content is None: |
| 144 continue | 151 continue |
| 145 # Gitiles encodes text content in base64 (see | 152 # Gitiles encodes text content in base64 (see |
| 146 # http://tools.ietf.org/html/rfc4648 for info about base64). | 153 # http://tools.ietf.org/html/rfc4648 for info about base64). |
| 147 value[path] = (list_dir if IsDirectory(path) else b64decode)(content) | 154 value[path] = (list_dir if IsDirectory(path) else b64decode)(content) |
| 148 return value | 155 return value |
| 149 | 156 |
| 150 return All(self._ResolveFetchContent(path, future, factory, skip_not_found) | 157 return All(self._ResolveFetchContent(path, future, skip_not_found) |
| 151 for path, future, factory in fetches).Then(parse_contents) | 158 for path, future in fetches).Then(parse_contents) |
| 152 | 159 |
| 153 def Refresh(self): | 160 def Refresh(self): |
| 154 return Future(value=()) | 161 return Future(value=()) |
| 155 | 162 |
| 156 @memoize | 163 @memoize |
| 157 def _GetCommitInfo(self, key): | 164 def _GetCommitInfo(self, key): |
| 158 '''Gets the commit information specified by |key|. | 165 '''Gets the commit information specified by |key|. |
| 159 | 166 |
| 160 The JSON view for commit info looks like: | 167 The JSON view for commit info looks like: |
| 161 { | 168 { |
| (...skipping 15 matching lines...) Expand all Loading... |
| 177 "message": "...", | 184 "message": "...", |
| 178 "tree_diff": [...] | 185 "tree_diff": [...] |
| 179 } | 186 } |
| 180 ''' | 187 ''' |
| 181 # Commit information for a branch is obtained by appending '?format=JSON' | 188 # Commit information for a branch is obtained by appending '?format=JSON' |
| 182 # to the branch URL. Note that '<gitiles_url>/<branch>?format=JSON' is | 189 # to the branch URL. Note that '<gitiles_url>/<branch>?format=JSON' is |
| 183 # different from '<gitiles_url>/<branch>/?format=JSON': the latter serves | 190 # different from '<gitiles_url>/<branch>/?format=JSON': the latter serves |
| 184 # the root directory JSON content, whereas the former serves the branch | 191 # the root directory JSON content, whereas the former serves the branch |
| 185 # commit info JSON content. | 192 # commit info JSON content. |
| 186 | 193 |
| 187 def make_fetch_future(): | 194 access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE) |
| 188 access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE) | 195 fetch_future = self._fetcher.FetchAsync(self._base_url + _JSON_FORMAT, |
| 189 return self._fetcher.FetchAsync(self._base_url + _JSON_FORMAT, | 196 access_token=access_token) |
| 190 access_token = access_token) | 197 content_future = self._ResolveFetchContent(self._base_url, fetch_future) |
| 191 | |
| 192 fetch_future = make_fetch_future() | |
| 193 content_future = self._ResolveFetchContent(self._base_url, fetch_future, | |
| 194 make_fetch_future) | |
| 195 return content_future.Then(lambda json: _ParseGitilesJson(json)[key]) | 198 return content_future.Then(lambda json: _ParseGitilesJson(json)[key]) |
| 196 | 199 |
| 197 def GetCommitID(self): | 200 def GetCommitID(self): |
| 198 '''Returns a future that resolves to the commit ID for this branch. | 201 '''Returns a future that resolves to the commit ID for this branch. |
| 199 ''' | 202 ''' |
| 200 return self._GetCommitInfo('commit') | 203 return self._GetCommitInfo('commit') |
| 201 | 204 |
| 202 def GetPreviousCommitID(self): | 205 def GetPreviousCommitID(self): |
| 203 '''Returns a future that resolves to the previous commit ID for this branch. | 206 '''Returns a future that resolves to the previous commit ID for this branch. |
| 204 ''' | 207 ''' |
| 205 return self._GetCommitInfo('parents').Then(lambda parents: parents[0]) | 208 return self._GetCommitInfo('parents').Then(lambda parents: parents[0]) |
| 206 | 209 |
| 207 def StatAsync(self, path): | 210 def StatAsync(self, path): |
| 208 dir_, filename = posixpath.split(path) | 211 dir_, filename = posixpath.split(path) |
| 209 def stat(content): | 212 def stat(content): |
| 210 stat_info = _CreateStatInfo(content) | 213 stat_info = _CreateStatInfo(content) |
| 211 if stat_info.version is None: | 214 if stat_info.version is None: |
| 212 raise FileSystemError('Failed to find version of dir %s' % dir_) | 215 raise FileSystemError('Failed to find version of dir %s' % dir_) |
| 213 if IsDirectory(path): | 216 if IsDirectory(path): |
| 214 return stat_info | 217 return stat_info |
| 215 if filename not in stat_info.child_versions: | 218 if filename not in stat_info.child_versions: |
| 216 raise FileNotFoundError( | 219 raise FileNotFoundError( |
| 217 '%s from %s was not in child versions for Stat' % (filename, path)) | 220 '%s from %s was not in child versions for Stat' % (filename, path)) |
| 218 return StatInfo(stat_info.child_versions[filename]) | 221 return StatInfo(stat_info.child_versions[filename]) |
| 219 | 222 |
| 220 def make_fetch_future(): | 223 fetch_future = self._FetchAsync(ToDirectory(dir_) + _JSON_FORMAT) |
| 221 return self._FetchAsync(ToDirectory(dir_) + _JSON_FORMAT) | 224 return self._ResolveFetchContent(path, fetch_future).Then(stat) |
| 222 | |
| 223 fetch_future = make_fetch_future() | |
| 224 return self._ResolveFetchContent(path, fetch_future, | |
| 225 make_fetch_future).Then(stat) | |
| 226 | 225 |
| 227 def GetIdentity(self): | 226 def GetIdentity(self): |
| 228 # NOTE: Do not use commit information to create the string identity. | 227 # NOTE: Do not use commit information to create the string identity. |
| 229 # Doing so will mess up caching. | 228 # Doing so will mess up caching. |
| 230 if self._commit is None and self._branch != 'master': | 229 if self._commit is None and self._branch != 'master': |
| 231 str_id = GITILES_BRANCH_BASE | 230 str_id = '%s/%s/%s/%s' % ( |
| 231 GITILES_BASE, GITILES_SRC_ROOT, GITILES_BRANCHES_PATH, self._branch) |
| 232 else: | 232 else: |
| 233 str_id = GITILES_BASE | 233 str_id = '%s/%s' % (GITILES_BASE, GITILES_SRC_ROOT) |
| 234 return '@'.join((self.__class__.__name__, StringIdentity(str_id))) | 234 return '@'.join((self.__class__.__name__, StringIdentity(str_id))) |
| OLD | NEW |