Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2012 The LUCI Authors. All rights reserved. | 1 # Copyright 2012 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """This module defines Isolate Server frontend url handlers.""" | 5 """This module defines Isolate Server frontend url handlers.""" |
| 6 | 6 |
| 7 import cgi | 7 import cgi |
| 8 from collections import OrderedDict | |
|
M-A Ruel
2017/03/23 21:23:42
just "import collections"
jonesmi
2017/03/23 22:15:29
Done.
| |
| 8 import datetime | 9 import datetime |
| 9 import json | 10 import json |
| 10 import logging | 11 import logging |
| 12 import os | |
| 11 import re | 13 import re |
| 14 import urllib | |
| 12 | 15 |
| 13 import webapp2 | 16 import webapp2 |
| 14 | 17 |
| 15 from google.appengine.api import memcache | 18 from google.appengine.api import memcache |
| 16 from google.appengine.api import modules | 19 from google.appengine.api import modules |
| 17 from google.appengine.api import users | 20 from google.appengine.api import users |
| 18 | 21 |
| 19 import acl | 22 import acl |
| 20 import config | 23 import config |
| 21 import gcs | 24 import gcs |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 51 'other_requests', | 54 'other_requests', |
| 52 'failures', | 55 'failures', |
| 53 'uploads', | 56 'uploads', |
| 54 'downloads', | 57 'downloads', |
| 55 'contains_requests', | 58 'contains_requests', |
| 56 'uploads_bytes', | 59 'uploads_bytes', |
| 57 'downloads_bytes', | 60 'downloads_bytes', |
| 58 'contains_lookups', | 61 'contains_lookups', |
| 59 ) | 62 ) |
| 60 | 63 |
| 64 _ISOLATED_ROOT_MEMBERS = ( | |
| 65 'algo', | |
| 66 'command', | |
| 67 'files', | |
| 68 'includes', | |
| 69 'read_only', | |
| 70 'relative_cwd', | |
| 71 'version', | |
| 72 ) | |
| 73 | |
| 61 | 74 |
| 62 ### Restricted handlers | 75 ### Restricted handlers |
| 63 | 76 |
| 64 | 77 |
| 65 class RestrictedConfigHandler(auth.AuthenticatingHandler): | 78 class RestrictedConfigHandler(auth.AuthenticatingHandler): |
| 66 @auth.autologin | 79 @auth.autologin |
| 67 @auth.require(auth.is_admin) | 80 @auth.require(auth.is_admin) |
| 68 def get(self): | 81 def get(self): |
| 69 self.common(None) | 82 self.common(None) |
| 70 | 83 |
| (...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 183 ### Non-restricted handlers | 196 ### Non-restricted handlers |
| 184 | 197 |
| 185 | 198 |
| 186 class BrowseHandler(auth.AuthenticatingHandler): | 199 class BrowseHandler(auth.AuthenticatingHandler): |
| 187 @auth.autologin | 200 @auth.autologin |
| 188 @auth.require(acl.isolate_readable) | 201 @auth.require(acl.isolate_readable) |
| 189 def get(self): | 202 def get(self): |
| 190 namespace = self.request.get('namespace', 'default-gzip') | 203 namespace = self.request.get('namespace', 'default-gzip') |
| 191 # Support 'hash' for compatibility with old links. To remove eventually. | 204 # Support 'hash' for compatibility with old links. To remove eventually. |
| 192 digest = self.request.get('digest', '') or self.request.get('hash', '') | 205 digest = self.request.get('digest', '') or self.request.get('hash', '') |
| 206 save_as = self.request.get('as', '') | |
| 193 params = { | 207 params = { |
| 208 u'as': unicode(save_as), | |
| 194 u'digest': unicode(digest), | 209 u'digest': unicode(digest), |
| 195 u'namespace': unicode(namespace), | 210 u'namespace': unicode(namespace), |
| 196 } | 211 } |
| 197 # Check for existence of element, so we can 400/404 | 212 # Check for existence of element, so we can 400/404 |
| 198 if digest and namespace: | 213 if digest and namespace: |
| 199 try: | 214 try: |
| 200 model.get_content(namespace, digest) | 215 model.get_content(namespace, digest) |
| 201 except ValueError: | 216 except ValueError: |
| 202 self.abort(400, 'Invalid key') | 217 self.abort(400, 'Invalid key') |
| 203 except LookupError: | 218 except LookupError: |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 250 if len(content) > 33554000: | 265 if len(content) > 33554000: |
| 251 host = modules.get_hostname(module='default', version='default') | 266 host = modules.get_hostname(module='default', version='default') |
| 252 # host is something like default.default.myisolateserver.appspot.com | 267 # host is something like default.default.myisolateserver.appspot.com |
| 253 host = host.replace('default.default.','') | 268 host = host.replace('default.default.','') |
| 254 sizeInMib = len(content) / (1024.0 * 1024.0) | 269 sizeInMib = len(content) / (1024.0 * 1024.0) |
| 255 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' | 270 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' |
| 256 ' App Engine limit.\nTo work around this, run the following command:\n' | 271 ' App Engine limit.\nTo work around this, run the following command:\n' |
| 257 ' python isolateserver.py download -I %s --namespace %s -f %s %s' | 272 ' python isolateserver.py download -I %s --namespace %s -f %s %s' |
| 258 % (sizeInMib, host, namespace, digest, digest)) | 273 % (sizeInMib, host, namespace, digest, digest)) |
| 259 else: | 274 else: |
| 260 self.response.headers['Content-Disposition'] = str('filename=%s' | 275 self.response.headers['Content-Disposition'] = str( |
| 261 % digest) | 276 'filename=%s' % self.request.get('as') or digest) |
| 262 if content.startswith('{'): | 277 try: |
| 263 # Try to format as JSON. | 278 json_data = json.loads(content) |
| 264 try: | 279 if self._is_isolated_format(json_data): |
| 265 content = json.dumps( | |
| 266 json.loads(content), sort_keys=True, indent=2, | |
| 267 separators=(',', ': ')) | |
| 268 content = cgi.escape(content) | |
| 269 # If we don't wrap this in html, browsers will put content in a pre | |
| 270 # tag which is also styled with monospace/pre-wrap. We can't use | |
| 271 # anchor tags in <pre>, so we force it to be a <div>, which happily | |
| 272 # accepts links. | |
| 273 content = ( | |
| 274 '<div style="font-family:monospace;white-space:pre-wrap;">%s' | |
| 275 '</div>' % content) | |
| 276 # Linkify things that look like hashes | |
| 277 content = re.sub(r'([0-9a-f]{40})', | |
| 278 r'<a target="_blank" href="/browse?namespace=%s' % namespace + | |
| 279 r'&digest=\1">\1</a>', | |
| 280 content) | |
| 281 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' | 280 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' |
| 282 except ValueError: | 281 json_data['files'] = OrderedDict( |
|
M-A Ruel
2017/03/23 21:23:42
collections.OrderedDict
jonesmi
2017/03/23 22:15:30
Done.
| |
| 283 pass | 282 sorted( |
| 283 json_data['files'].items(), | |
| 284 key=lambda (filepath, data): filepath)) | |
| 285 params = { | |
| 286 'namespace': namespace, | |
| 287 'isolated': json_data, | |
| 288 } | |
| 289 content = template.render('isolate/isolated.html', params) | |
| 290 except ValueError: | |
| 291 pass | |
| 284 | 292 |
| 285 self.response.write(content) | 293 self.response.write(content) |
| 286 | 294 |
| 295 @staticmethod | |
| 296 def _is_isolated_format(json_data): | |
| 297 """Checks if json_data is a valid .isolated format.""" | |
| 298 if not isinstance(json_data, dict): | |
| 299 return False | |
| 300 actual = set(json_data) | |
| 301 return actual.issubset(_ISOLATED_ROOT_MEMBERS) and 'files' in actual | |
| 302 | |
| 287 | 303 |
| 288 class StatsHandler(webapp2.RequestHandler): | 304 class StatsHandler(webapp2.RequestHandler): |
| 289 """Returns the statistics web page.""" | 305 """Returns the statistics web page.""" |
| 290 def get(self): | 306 def get(self): |
| 291 """Presents nice recent statistics. | 307 """Presents nice recent statistics. |
| 292 | 308 |
| 293 It fetches data from the 'JSON' API. | 309 It fetches data from the 'JSON' API. |
| 294 """ | 310 """ |
| 295 # Preloads the data to save a complete request. | 311 # Preloads the data to save a complete request. |
| 296 resolution = self.request.params.get('resolution', 'hours') | 312 resolution = self.request.params.get('resolution', 'hours') |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 411 def create_application(debug): | 427 def create_application(debug): |
| 412 """Creates the url router. | 428 """Creates the url router. |
| 413 | 429 |
| 414 The basic layouts is as follow: | 430 The basic layouts is as follow: |
| 415 - /restricted/.* requires being an instance administrator. | 431 - /restricted/.* requires being an instance administrator. |
| 416 - /stats/.* has statistics. | 432 - /stats/.* has statistics. |
| 417 """ | 433 """ |
| 418 acl.bootstrap() | 434 acl.bootstrap() |
| 419 template.bootstrap() | 435 template.bootstrap() |
| 420 return webapp2.WSGIApplication(get_routes(), debug=debug) | 436 return webapp2.WSGIApplication(get_routes(), debug=debug) |
| OLD | NEW |