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