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

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

Issue 2693953006: Isolate: Download files as their filename instead of hash (Closed)
Patch Set: 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
12 13
13 import webapp2 14 import webapp2
14 15
15 from google.appengine.api import memcache 16 from google.appengine.api import memcache
16 from google.appengine.api import modules 17 from google.appengine.api import modules
17 from google.appengine.api import users 18 from google.appengine.api import users
18 19
19 import acl 20 import acl
20 import config 21 import config
(...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after
184 ### Non-restricted handlers 185 ### Non-restricted handlers
185 186
186 187
187 class BrowseHandler(auth.AuthenticatingHandler): 188 class BrowseHandler(auth.AuthenticatingHandler):
188 @auth.autologin 189 @auth.autologin
189 @auth.require(acl.isolate_readable) 190 @auth.require(acl.isolate_readable)
190 def get(self): 191 def get(self):
191 namespace = self.request.get('namespace', 'default-gzip') 192 namespace = self.request.get('namespace', 'default-gzip')
192 # Support 'hash' for compatibility with old links. To remove eventually. 193 # Support 'hash' for compatibility with old links. To remove eventually.
193 digest = self.request.get('digest', '') or self.request.get('hash', '') 194 digest = self.request.get('digest', '') or self.request.get('hash', '')
195 save_as = self.request.get('as', '')
194 params = { 196 params = {
195 u'digest': unicode(digest), 197 u'digest': unicode(digest),
196 u'namespace': unicode(namespace), 198 u'namespace': unicode(namespace),
199 u'as': unicode(save_as),
M-A Ruel 2017/02/14 21:01:16 keep keys sorted
jonesmi 2017/02/14 23:01:18 Done.
197 } 200 }
198 # Check for existence of element, so we can 400/404 201 # Check for existence of element, so we can 400/404
199 if digest and namespace: 202 if digest and namespace:
200 try: 203 try:
201 model.get_content(namespace, digest) 204 model.get_content(namespace, digest)
202 except ValueError: 205 except ValueError:
203 self.abort(400, 'Invalid key') 206 self.abort(400, 'Invalid key')
204 except LookupError: 207 except LookupError:
205 self.abort(404, 'Unable to retrieve the entry') 208 self.abort(404, 'Unable to retrieve the entry')
206 self.response.write(template.render('isolate/browse.html', params)) 209 self.response.write(template.render('isolate/browse.html', params))
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
251 if len(content) > 33554000: 254 if len(content) > 33554000:
252 host = modules.get_hostname(module='default', version='default') 255 host = modules.get_hostname(module='default', version='default')
253 # host is something like default.default.myisolateserver.appspot.com 256 # host is something like default.default.myisolateserver.appspot.com
254 host = host.replace('default.default.','') 257 host = host.replace('default.default.','')
255 sizeInMib = len(content) / (1024.0 * 1024.0) 258 sizeInMib = len(content) / (1024.0 * 1024.0)
256 content = ('Sorry, your file is %1.1f MiB big, which exceeds the 32 MiB' 259 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' 260 ' App Engine limit.\nTo work around this, run the following command:\n'
258 ' python isolateserver.py download -I %s --namespace %s -f %s %s' 261 ' python isolateserver.py download -I %s --namespace %s -f %s %s'
259 % (sizeInMib, host, namespace, digest, digest)) 262 % (sizeInMib, host, namespace, digest, digest))
260 else: 263 else:
261 self.response.headers['Content-Disposition'] = str('filename=%s' 264 self.response.headers['Content-Disposition'] = str(
262 % digest) 265 'filename=%s' % self.request.get('as') or digest)
263 if content.startswith('{'): 266 if content.startswith('{'):
264 # Try to format as JSON. 267 # Try to format as JSON.
265 try: 268 content = self._format_content_as_json(content, namespace)
266 content = json.dumps( 269 self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
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 270
286 self.response.write(content) 271 self.response.write(content)
287 272
273 @staticmethod
274 def _format_content_as_json(content, namespace):
275 """Formats the content as json, and returns a string repr of the content."""
276 try:
277 # Ensure we're working with HTML-safe content. Do this before adding
278 # our own hyperlinks because cgi.escape would replace our anchor symbols.
279 content = cgi.escape(content)
M-A Ruel 2017/02/14 21:01:16 $ python -c 'import cgi,json; print json.loads(cgi
jonesmi 2017/02/14 23:01:18 Sounds good. Now doing the checks in a static meth
M-A Ruel 2017/02/14 23:46:14 There is still no guarantee that someone uploaded
jonesmi 2017/02/16 19:17:30 Ahh I see. Nice catch, thanks! We have to do cgi.e
280 data = json.loads(content)
281 except ValueError:
282 return content
283
284 # Linkify all files
285 if 'files' in data:
M-A Ruel 2017/02/14 21:01:16 Let's add some more checks. Ref: https://github.co
jonesmi 2017/02/14 23:01:18 Done, although it seems superfluous to check actua
286 hyperlinked_files = {}
287 for filepath, metadata in data['files'].iteritems():
288 if 'h' in metadata: # Only linkify files that have a digest property
289 save_as = os.path.basename(filepath)
290 anchor = (r'<a target="_blank" '
M-A Ruel 2017/02/14 21:01:16 remove the quotes
jonesmi 2017/02/14 23:01:18 Done.
291 r'href="/browse?namespace=%s&digest=%s&as=%s">'
M-A Ruel 2017/02/14 21:01:16 remove the quotes, and use urllib.quote(save_as) s
jonesmi 2017/02/14 23:01:18 Done.
292 r'%s</a>') % (namespace, metadata['h'], save_as, filepath)
293 hyperlinked_files[anchor] = metadata
294 data['files'] = hyperlinked_files
295
296 content = json.dumps(data, sort_keys=True, indent=2, separators=(',', ': '))
297 # The string representation from json.dumps force-escapes all quotes,
M-A Ruel 2017/02/14 21:01:16 This is not needed anymore, as there's no quotes w
jonesmi 2017/02/14 23:01:18 Done.
298 # which would break our href strings. Replace these with regular quotes.
299 content = content.replace(r'\"', '"')
300
301 # If we don't wrap this in html, browsers will put content in a pre
302 # tag which is also styled with monospace/pre-wrap. We can't use
303 # anchor tags in <pre>, so we force it to be a <div>, which happily
304 # accepts links.
305 content = (
306 '<div style="font-family:monospace;white-space:pre-wrap;">%s'
307 '</div>' % content)
308 return content
309
288 310
289 class StatsHandler(webapp2.RequestHandler): 311 class StatsHandler(webapp2.RequestHandler):
290 """Returns the statistics web page.""" 312 """Returns the statistics web page."""
291 def get(self): 313 def get(self):
292 """Presents nice recent statistics. 314 """Presents nice recent statistics.
293 315
294 It fetches data from the 'JSON' API. 316 It fetches data from the 'JSON' API.
295 """ 317 """
296 # Preloads the data to save a complete request. 318 # Preloads the data to save a complete request.
297 resolution = self.request.params.get('resolution', 'hours') 319 resolution = self.request.params.get('resolution', 'hours')
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
412 def create_application(debug): 434 def create_application(debug):
413 """Creates the url router. 435 """Creates the url router.
414 436
415 The basic layouts is as follow: 437 The basic layouts is as follow:
416 - /restricted/.* requires being an instance administrator. 438 - /restricted/.* requires being an instance administrator.
417 - /stats/.* has statistics. 439 - /stats/.* has statistics.
418 """ 440 """
419 acl.bootstrap() 441 acl.bootstrap()
420 template.bootstrap() 442 template.bootstrap()
421 return webapp2.WSGIApplication(get_routes(), debug=debug) 443 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