Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(159)

Side by Side Diff: appengine/isolate/handlers_frontend.py

Issue 2693953006: Isolate: Download files as their filename instead of hash (Closed)
Patch Set: Isolate: Download files as their filename instead of hash Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | appengine/isolate/handlers_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 import datetime 8 import datetime
9 import json 9 import json
10 import logging 10 import logging
11 import os
11 import re 12 import re
13 import urllib
12 14
13 import webapp2 15 import webapp2
14 16
15 from google.appengine.api import memcache 17 from google.appengine.api import memcache
16 from google.appengine.api import modules 18 from google.appengine.api import modules
17 from google.appengine.api import users 19 from google.appengine.api import users
18 20
19 import acl 21 import acl
20 import config 22 import config
21 import gcs 23 import gcs
(...skipping 29 matching lines...) Expand all
51 'other_requests', 53 'other_requests',
52 'failures', 54 'failures',
53 'uploads', 55 'uploads',
54 'downloads', 56 'downloads',
55 'contains_requests', 57 'contains_requests',
56 'uploads_bytes', 58 'uploads_bytes',
57 'downloads_bytes', 59 'downloads_bytes',
58 'contains_lookups', 60 'contains_lookups',
59 ) 61 )
60 62
63 _ISOLATED_ROOT_MEMBERS = (
64 'algo',
65 'command',
66 'files',
67 'includes',
68 'read_only',
69 'relative_cwd',
70 'version',
71 )
72
61 73
62 ### Restricted handlers 74 ### Restricted handlers
63 75
64 76
65 class RestrictedConfigHandler(auth.AuthenticatingHandler): 77 class RestrictedConfigHandler(auth.AuthenticatingHandler):
66 @auth.autologin 78 @auth.autologin
67 @auth.require(auth.is_admin) 79 @auth.require(auth.is_admin)
68 def get(self): 80 def get(self):
69 self.common(None) 81 self.common(None)
70 82
(...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after
184 ### Non-restricted handlers 196 ### Non-restricted handlers
185 197
186 198
187 class BrowseHandler(auth.AuthenticatingHandler): 199 class BrowseHandler(auth.AuthenticatingHandler):
188 @auth.autologin 200 @auth.autologin
189 @auth.require(acl.isolate_readable) 201 @auth.require(acl.isolate_readable)
190 def get(self): 202 def get(self):
191 namespace = self.request.get('namespace', 'default-gzip') 203 namespace = self.request.get('namespace', 'default-gzip')
192 # Support 'hash' for compatibility with old links. To remove eventually. 204 # Support 'hash' for compatibility with old links. To remove eventually.
193 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', '')
194 params = { 207 params = {
208 u'as': unicode(save_as),
195 u'digest': unicode(digest), 209 u'digest': unicode(digest),
196 u'namespace': unicode(namespace), 210 u'namespace': unicode(namespace),
197 } 211 }
198 # Check for existence of element, so we can 400/404 212 # Check for existence of element, so we can 400/404
199 if digest and namespace: 213 if digest and namespace:
200 try: 214 try:
201 model.get_content(namespace, digest) 215 model.get_content(namespace, digest)
202 except ValueError: 216 except ValueError:
203 self.abort(400, 'Invalid key') 217 self.abort(400, 'Invalid key')
204 except LookupError: 218 except LookupError:
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
251 if len(content) > 33554000: 265 if len(content) > 33554000:
252 host = modules.get_hostname(module='default', version='default') 266 host = modules.get_hostname(module='default', version='default')
253 # host is something like default.default.myisolateserver.appspot.com 267 # host is something like default.default.myisolateserver.appspot.com
254 host = host.replace('default.default.','') 268 host = host.replace('default.default.','')
255 sizeInMib = len(content) / (1024.0 * 1024.0) 269 sizeInMib = len(content) / (1024.0 * 1024.0)
256 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'
257 ' 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'
258 ' python isolateserver.py download -I %s --namespace %s -f %s %s' 272 ' python isolateserver.py download -I %s --namespace %s -f %s %s'
259 % (sizeInMib, host, namespace, digest, digest)) 273 % (sizeInMib, host, namespace, digest, digest))
260 else: 274 else:
261 self.response.headers['Content-Disposition'] = str('filename=%s' 275 self.response.headers['Content-Disposition'] = str(
262 % digest) 276 'filename=%s' % self.request.get('as') or digest)
263 if content.startswith('{'): 277 try:
264 # Try to format as JSON. 278 json_data = json.loads(content)
265 try: 279 if self._is_isolated_format(json_data):
266 content = json.dumps( 280 content = self._format_isolated_content(json_data, namespace)
267 json.loads(content), sort_keys=True, indent=2,
268 separators=(',', ': '))
269 content = cgi.escape(content)
270 # If we don't wrap this in html, browsers will put content in a pre
271 # tag which is also styled with monospace/pre-wrap. We can't use
272 # anchor tags in <pre>, so we force it to be a <div>, which happily
273 # accepts links.
274 content = (
275 '<div style="font-family:monospace;white-space:pre-wrap;">%s'
276 '</div>' % content)
277 # Linkify things that look like hashes
278 content = re.sub(r'([0-9a-f]{40})',
279 r'<a target="_blank" href="/browse?namespace=%s' % namespace +
280 r'&digest=\1">\1</a>',
281 content)
282 self.response.headers['Content-Type'] = 'text/html; charset=utf-8' 281 self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
283 except ValueError: 282 except ValueError:
284 pass 283 pass
285 284
286 self.response.write(content) 285 self.response.write(content)
287 286
287 @staticmethod
288 def _is_isolated_format(json_data):
289 """Checks if json_data is a valid .isolated format."""
290 if isinstance(json_data, dict):
M-A Ruel 2017/02/16 21:29:12 for "checker" functions, use early return instead;
jonesmi 2017/02/22 21:43:18 Done.
291 actual = set(json_data)
292 return actual.issubset(_ISOLATED_ROOT_MEMBERS) and 'files' in actual
293 return False
294
295 @staticmethod
M-A Ruel 2017/02/16 21:29:12 @classmethod def _escape_all_dict_strings(cls, d):
mithro 2017/02/16 23:21:35 Guide from security - DON'T TRY AND ESCAPE STUFF Y
jonesmi 2017/02/22 21:43:18 Looks like we've already configured our jinja2 to
296 def _escape_all_dict_strings(d):
297 """Recursively modify every str val in json dict to be cgi.escape()'d."""
298 for k, v in d.iteritems():
299 if isinstance(v, str):
M-A Ruel 2017/02/16 21:29:12 basestring
jonesmi 2017/02/22 21:43:18 Ack, however getting rid of this method in favor o
300 d[k] = cgi.escape(v)
301 elif isinstance(v, dict):
M-A Ruel 2017/02/16 21:29:12 you need to iterate in lists too; so the function
jonesmi 2017/02/22 21:43:18 Ack, however getting rid of this method in favor o
302 d[k] = ContentHandler._escape_all_dict_strings(v)
M-A Ruel 2017/02/16 21:29:12 d[k] = cls._escape_all_dict_strings(v)
jonesmi 2017/02/22 21:43:18 Ack, however getting rid of this method in favor o
303 return d
304
305 @staticmethod
M-A Ruel 2017/02/16 21:29:12 classmethod
jonesmi 2017/02/22 21:43:18 Done.
306 def _format_isolated_content(json_data, namespace):
307 """Formats .isolated content and returns a string representation of it."""
308 # Ensure we're working with HTML-safe content. Do this before adding
309 # our own hyperlinks because cgi.escape would replace our anchor symbols.
310 json_data = ContentHandler._escape_all_dict_strings(json_data)
311
312 # Linkify all files
313 if 'files' in json_data:
M-A Ruel 2017/02/16 21:29:12 check is not needed anymore.
jonesmi 2017/02/22 21:43:18 Done.
314 hyperlinked_files = {}
315 for filepath, metadata in json_data['files'].iteritems():
316 if metadata.get('h'):
317 save_as = os.path.basename(filepath)
318 anchor = (r'<a target=_blank" '
319 r'href=/browse?namespace=%s&digest=%s&as=%s>'
320 r'%s</a>') % (
321 namespace, metadata['h'], urllib.quote(save_as), filepath)
322 hyperlinked_files[anchor] = metadata
323 json_data['files'] = hyperlinked_files
324
325 content = json.dumps(
326 json_data, sort_keys=True, indent=2, separators=(',', ': '))
327 # If we don't wrap this in html, browsers will put content in a pre
328 # tag which is also styled with monospace/pre-wrap. We can't use
329 # anchor tags in <pre>, so we force it to be a <div>, which happily
330 # accepts links.
331 content = (
332 '<div style="font-family:monospace;white-space:pre-wrap;">%s'
333 '</div>' % content)
334 return content
335
288 336
289 class StatsHandler(webapp2.RequestHandler): 337 class StatsHandler(webapp2.RequestHandler):
290 """Returns the statistics web page.""" 338 """Returns the statistics web page."""
291 def get(self): 339 def get(self):
292 """Presents nice recent statistics. 340 """Presents nice recent statistics.
293 341
294 It fetches data from the 'JSON' API. 342 It fetches data from the 'JSON' API.
295 """ 343 """
296 # Preloads the data to save a complete request. 344 # Preloads the data to save a complete request.
297 resolution = self.request.params.get('resolution', 'hours') 345 resolution = self.request.params.get('resolution', 'hours')
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
412 def create_application(debug): 460 def create_application(debug):
413 """Creates the url router. 461 """Creates the url router.
414 462
415 The basic layouts is as follow: 463 The basic layouts is as follow:
416 - /restricted/.* requires being an instance administrator. 464 - /restricted/.* requires being an instance administrator.
417 - /stats/.* has statistics. 465 - /stats/.* has statistics.
418 """ 466 """
419 acl.bootstrap() 467 acl.bootstrap()
420 template.bootstrap() 468 template.bootstrap()
421 return webapp2.WSGIApplication(get_routes(), debug=debug) 469 return webapp2.WSGIApplication(get_routes(), debug=debug)
OLDNEW
« no previous file with comments | « no previous file | appengine/isolate/handlers_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698