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

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

Issue 1866753008: Add ability to linkify hashes on isolate server (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@master
Patch Set: Address feedback Created 4 years, 8 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/model.py » ('j') | appengine/isolate/model.py » ('J')
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 by the Apache v2.0 license that can be 2 # Use of this source code is governed by the Apache v2.0 license that can be
3 # found in the LICENSE file. 3 # 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 datetime 7 import datetime
8 import json 8 import json
9 import re
9 10
10 import webapp2 11 import webapp2
11 12
12 from google.appengine.api import memcache 13 from google.appengine.api import memcache
13 from google.appengine.api import users 14 from google.appengine.api import users
14 15
15 import acl 16 import acl
16 import config 17 import config
17 import gcs 18 import gcs
18 import mapreduce_jobs 19 import mapreduce_jobs
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after
144 ### Non-restricted handlers 145 ### Non-restricted handlers
145 146
146 147
147 class BrowseHandler(auth.AuthenticatingHandler): 148 class BrowseHandler(auth.AuthenticatingHandler):
148 @auth.autologin 149 @auth.autologin
149 @auth.require(acl.isolate_readable) 150 @auth.require(acl.isolate_readable)
150 def get(self): 151 def get(self):
151 namespace = self.request.get('namespace', 'default-gzip') 152 namespace = self.request.get('namespace', 'default-gzip')
152 # Support 'hash' for compatibility with old links. To remove eventually. 153 # Support 'hash' for compatibility with old links. To remove eventually.
153 digest = self.request.get('digest', '') or self.request.get('hash', '') 154 digest = self.request.get('digest', '') or self.request.get('hash', '')
155 params = {
156 u'digest': unicode(digest),
157 u'namespace': unicode(namespace),
158 u'onload': '',
159 }
160 # Check for existence of element, so we can 400/404
161 if digest and namespace:
162 try:
163 model.get_content(namespace, digest)
164 except ValueError:
165 self.abort(400, 'Invalid key')
166 except LookupError:
167 self.abort(404, 'Unable to retrieve the entry')
M-A Ruel 2016/04/14 14:05:06 alignment
kjlubick 2016/04/14 14:57:37 Done.
168 self.response.write(template.render('isolate/browse.html', params))
169
170
171 class ContentHandler(auth.AuthenticatingHandler):
172 @auth.autologin
173 @auth.require(acl.isolate_readable)
174 def get(self):
175 namespace = self.request.get('namespace', 'default-gzip')
176 digest = self.request.get('digest', '')
177
154 content = None 178 content = None
M-A Ruel 2016/04/14 14:05:06 This can be removed.
kjlubick 2016/04/14 14:57:37 Done.
155 if digest and namespace: 179 if digest and namespace:
156 # TODO(maruel): Refactor into a function. 180 raw_data = None
M-A Ruel 2016/04/14 14:05:06 You need to move up above the condition, otherwise
kjlubick 2016/04/14 14:57:37 Good catch. Done.
157 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) 181 try:
158 if memcache_entry is not None: 182 raw_data = model.get_content(namespace, digest)
159 raw_data = memcache_entry 183 except ValueError:
160 else: 184 self.abort(400, 'Invalid key')
161 try: 185 except LookupError:
162 key = model.get_entry_key(namespace, digest) 186 self.abort(404, 'Unable to retrieve the entry')
163 except ValueError: 187
164 self.abort(400, 'Invalid key')
165 entity = key.get()
166 if entity is None:
167 self.abort(404, 'Unable to retrieve the entry')
168 raw_data = entity.content
169 if not raw_data: 188 if not raw_data:
189 key = model.get_entry_key(namespace, digest)
M-A Ruel 2016/04/14 14:05:06 In this case, the entity is effectively loaded twi
kjlubick 2016/04/14 14:57:37 Done. entity is returned as the second argument.
170 stream = gcs.read_file(config.settings().gs_bucket, key.id()) 190 stream = gcs.read_file(config.settings().gs_bucket, key.id())
171 else: 191 else:
172 stream = [raw_data] 192 stream = [raw_data]
173 content = ''.join(model.expand_content(namespace, stream)) 193 content = ''.join(model.expand_content(namespace, stream))
194
195 self.response.headers['X-Frame-Options'] = 'SAMEORIGIN'
196 del self.response.headers['Content-Type']
197 # Apparently, setting the content type to text/plain encourages the
198 # browser (Chrome, at least) to sniff the mime type and display
199 # things like images. Images are autowrapped in <img> and text is
200 # wrapped in <pre>.
201 self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
202 self.response.headers['Content-Disposition'] = str('filename=%s' % digest)
174 if content.startswith('{'): 203 if content.startswith('{'):
175 # Try to format as JSON. 204 # Try to format as JSON.
176 try: 205 try:
177 content = json.dumps( 206 content = json.dumps(
178 json.loads(content), sort_keys=True, indent=2, 207 json.loads(content), sort_keys=True, indent=2,
179 separators=(',', ': ')) 208 separators=(',', ': '))
209 # If we don't wrap this in html, browsers will put content in a pre
M-A Ruel 2016/04/14 14:05:06 Thanks for the explanation, I didn't know this.
210 # tag which is also styled with monospace/pre-wrap. We can't use
211 # anchor tags in <pre>, so we force it to be a <div>, which happily
212 # accepts links.
213 content = (
214 '<div style="font-family:monospace;white-space:pre-wrap;">%s</div>'
215 % content)
216 # Linkify things that look like hashes
217 content = re.sub(r'([0-9a-f]{40})',
218 r'<a target="_blank" href="browse?namespace=default-gzip' +
M-A Ruel 2016/04/14 14:05:06 use the namespcae instead of hardcoding 'default-g
kjlubick 2016/04/14 14:57:37 Done.
219 r'&digest=\1">\1</a>',
220 content)
221 self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
180 except ValueError: 222 except ValueError:
181 pass 223 pass
182 content = content.decode('utf8', 'replace') 224
183 params = { 225 self.response.write(content)
184 u'content': content,
185 u'digest': unicode(digest),
186 u'namespace': unicode(namespace),
187 # TODO(maruel): Add back once Web UI authentication is switched to OAuth2.
188 #'onload': 'update()' if digest else '',
189 u'onload': '',
190 }
191 self.response.write(template.render('isolate/browse.html', params))
192 226
193 227
194 class StatsHandler(webapp2.RequestHandler): 228 class StatsHandler(webapp2.RequestHandler):
195 """Returns the statistics web page.""" 229 """Returns the statistics web page."""
196 def get(self): 230 def get(self):
197 """Presents nice recent statistics. 231 """Presents nice recent statistics.
198 232
199 It fetches data from the 'JSON' API. 233 It fetches data from the 'JSON' API.
200 """ 234 """
201 # Preloads the data to save a complete request. 235 # Preloads the data to save a complete request.
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after
291 # Administrative urls. 325 # Administrative urls.
292 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), 326 webapp2.Route(r'/restricted/config', RestrictedConfigHandler),
293 327
294 # Mapreduce related urls. 328 # Mapreduce related urls.
295 webapp2.Route( 329 webapp2.Route(
296 r'/restricted/launch_mapreduce', 330 r'/restricted/launch_mapreduce',
297 RestrictedLaunchMapReduceJob), 331 RestrictedLaunchMapReduceJob),
298 332
299 # User web pages. 333 # User web pages.
300 webapp2.Route(r'/browse', BrowseHandler), 334 webapp2.Route(r'/browse', BrowseHandler),
335 webapp2.Route(r'/content', ContentHandler),
301 webapp2.Route(r'/stats', StatsHandler), 336 webapp2.Route(r'/stats', StatsHandler),
302 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), 337 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler),
303 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), 338 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler),
304 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), 339 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler),
305 webapp2.Route(r'/', RootHandler), 340 webapp2.Route(r'/', RootHandler),
306 341
307 # AppEngine-specific urls: 342 # AppEngine-specific urls:
308 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), 343 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler),
309 webapp2.Route(r'/_ah/warmup', WarmupHandler), 344 webapp2.Route(r'/_ah/warmup', WarmupHandler),
310 ] 345 ]
311 346
312 347
313 def create_application(debug): 348 def create_application(debug):
314 """Creates the url router. 349 """Creates the url router.
315 350
316 The basic layouts is as follow: 351 The basic layouts is as follow:
317 - /restricted/.* requires being an instance administrator. 352 - /restricted/.* requires being an instance administrator.
318 - /stats/.* has statistics. 353 - /stats/.* has statistics.
319 """ 354 """
320 acl.bootstrap() 355 acl.bootstrap()
321 template.bootstrap() 356 template.bootstrap()
322 return webapp2.WSGIApplication(get_routes(), debug=debug) 357 return webapp2.WSGIApplication(get_routes(), debug=debug)
OLDNEW
« no previous file with comments | « no previous file | appengine/isolate/model.py » ('j') | appengine/isolate/model.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698