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

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'
M-A Ruel 2017/02/14 23:46:14 include trailing comma, so when a new line is adde
jonesmi 2017/02/16 19:17:30 Done.
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 if self._is_isolated_format(content):
264 # Try to format as JSON. 278 content = self._format_isolated_content(content, namespace)
265 try: 279 self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
266 content = json.dumps(
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'
283 except ValueError:
284 pass
285 280
286 self.response.write(content) 281 self.response.write(content)
287 282
283 @staticmethod
284 def _is_isolated_format(content):
285 """Checks if content string is a valid .isolated format."""
286 try:
287 data = json.loads(content)
288 except ValueError:
289 return False
290 if isinstance(data, dict):
291 actual = set(data)
292 if actual.issubset(_ISOLATED_ROOT_MEMBERS) and 'files' in actual:
M-A Ruel 2017/02/14 23:46:14 return actual.issubset(_ISOLATED_ROOT_MEMBERS) and
jonesmi 2017/02/16 19:17:30 Done.
293 return True
294 return False
295
296 @staticmethod
297 def _format_isolated_content(content, namespace):
mithro 2017/02/16 00:50:52 I feel like just giving the isolate json to a temp
298 """Formats .isolated content and returns a string representation of it."""
299 try:
300 # Ensure we're working with HTML-safe content. Do this before adding
301 # our own hyperlinks because cgi.escape would replace our anchor symbols.
302 content = cgi.escape(content)
303 data = json.loads(content)
304 except ValueError:
305 return content
306
307 # Linkify all files
308 if 'files' in data:
309 hyperlinked_files = {}
310 for filepath, metadata in data['files'].iteritems():
311 if 'h' in metadata: # Only linkify files that have a digest property
M-A Ruel 2017/02/14 23:46:14 if metadata.get('h'): is safer. no need for the co
jonesmi 2017/02/16 19:17:30 Done
312 save_as = os.path.basename(filepath)
313 anchor = (r'<a target=_blank" '
314 r'href=/browse?namespace=%s&digest=%s&as=%s>'
315 r'%s</a>') % (
316 namespace, metadata['h'], urllib.quote(save_as), filepath)
317 hyperlinked_files[anchor] = metadata
318 data['files'] = hyperlinked_files
319
320 content = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': '))
321 # If we don't wrap this in html, browsers will put content in a pre
322 # tag which is also styled with monospace/pre-wrap. We can't use
323 # anchor tags in <pre>, so we force it to be a <div>, which happily
324 # accepts links.
325 content = (
326 '<div style="font-family:monospace;white-space:pre-wrap;">%s'
327 '</div>' % content)
328 return content
329
288 330
289 class StatsHandler(webapp2.RequestHandler): 331 class StatsHandler(webapp2.RequestHandler):
290 """Returns the statistics web page.""" 332 """Returns the statistics web page."""
291 def get(self): 333 def get(self):
292 """Presents nice recent statistics. 334 """Presents nice recent statistics.
293 335
294 It fetches data from the 'JSON' API. 336 It fetches data from the 'JSON' API.
295 """ 337 """
296 # Preloads the data to save a complete request. 338 # Preloads the data to save a complete request.
297 resolution = self.request.params.get('resolution', 'hours') 339 resolution = self.request.params.get('resolution', 'hours')
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
412 def create_application(debug): 454 def create_application(debug):
413 """Creates the url router. 455 """Creates the url router.
414 456
415 The basic layouts is as follow: 457 The basic layouts is as follow:
416 - /restricted/.* requires being an instance administrator. 458 - /restricted/.* requires being an instance administrator.
417 - /stats/.* has statistics. 459 - /stats/.* has statistics.
418 """ 460 """
419 acl.bootstrap() 461 acl.bootstrap()
420 template.bootstrap() 462 template.bootstrap()
421 return webapp2.WSGIApplication(get_routes(), debug=debug) 463 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