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

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: Also add content sniffing 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
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 # TODO(maruel): Add back once Web UI authentication is switched to OAuth2.
M-A Ruel 2016/04/13 20:32:09 Remove this while at it, not needed.
kjlubick 2016/04/14 13:18:43 Done.
kjlubick 2016/04/14 13:18:43 Done.
159 #'onload': 'update()' if digest else '',
160 u'onload': '',
161 }
162 # Check for existence of element, so we can 400/404
163 if digest and namespace:
164 # TODO(maruel): Refactor into a function.
M-A Ruel 2016/04/13 20:32:09 Can you make it a function now that it is used at
kjlubick 2016/04/14 13:18:43 I put it in model.py and used it twice here. I co
165 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace)
166 if memcache_entry is None:
167 try:
168 key = model.get_entry_key(namespace, digest)
169 except ValueError:
170 self.abort(400, 'Invalid key')
171 entity = key.get()
172 if entity is None:
173 self.abort(404, 'Unable to retrieve the entry')
174 self.response.write(template.render('isolate/browse.html', params))
175
M-A Ruel 2016/04/13 20:32:09 2 empty lines between file level symbols
kjlubick 2016/04/14 13:18:43 Done.
176 class ContentHandler(auth.AuthenticatingHandler):
177 @auth.autologin
178 @auth.require(acl.isolate_readable)
179 def get(self):
180 namespace = self.request.get('namespace', 'default-gzip')
181 digest = self.request.get('digest', '')
182
154 content = None 183 content = None
155 if digest and namespace: 184 if digest and namespace:
156 # TODO(maruel): Refactor into a function. 185 # TODO(maruel): Refactor into a function.
157 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) 186 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace)
158 if memcache_entry is not None: 187 if memcache_entry is not None:
159 raw_data = memcache_entry 188 raw_data = memcache_entry
160 else: 189 else:
161 try: 190 try:
162 key = model.get_entry_key(namespace, digest) 191 key = model.get_entry_key(namespace, digest)
163 except ValueError: 192 except ValueError:
164 self.abort(400, 'Invalid key') 193 self.abort(400, 'Invalid key')
165 entity = key.get() 194 entity = key.get()
166 if entity is None: 195 if entity is None:
167 self.abort(404, 'Unable to retrieve the entry') 196 self.abort(404, 'Unable to retrieve the entry')
168 raw_data = entity.content 197 raw_data = entity.content
169 if not raw_data: 198 if not raw_data:
170 stream = gcs.read_file(config.settings().gs_bucket, key.id()) 199 stream = gcs.read_file(config.settings().gs_bucket, key.id())
171 else: 200 else:
172 stream = [raw_data] 201 stream = [raw_data]
173 content = ''.join(model.expand_content(namespace, stream)) 202 content = ''.join(model.expand_content(namespace, stream))
203
204 self.response.headers['X-Frame-Options'] = 'SAMEORIGIN'
205 del self.response.headers['Content-Type']
206 # Apparently, setting the content type to text/plain encourages the
207 # browser (Chrome, at least) to sniff the mime type and display
208 # things like images. Images are autowrapped in <img> and text is
209 # wrapped in <pre>.
210 self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
211 self.response.headers['Content-Disposition'] = str('filename=%s' % digest)
174 if content.startswith('{'): 212 if content.startswith('{'):
175 # Try to format as JSON. 213 # Try to format as JSON.
176 try: 214 try:
177 content = json.dumps( 215 content = json.dumps(
178 json.loads(content), sort_keys=True, indent=2, 216 json.loads(content), sort_keys=True, indent=2,
179 separators=(',', ': ')) 217 separators=(',', ': '))
218 content = (
219 '<div style="font-family:monospace;white-space:pre-wrap;">%s</div>'
M-A Ruel 2016/04/13 20:32:09 Looks like this is not strictly necessary from wha
kjlubick 2016/04/14 13:18:43 I'll add a comment that mirrors this, but I don't
220 % content)
221 # Linkify things that look like hashes
222 content = re.sub(r'([0-9a-f]{40})',
223 r"""<a target="_blank" href="browse?namespace=default-gzip
224 &digest=\1">\1</a>""",
225 content)
226 self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
180 except ValueError: 227 except ValueError:
181 pass 228 pass
182 content = content.decode('utf8', 'replace') 229
183 params = { 230 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 231
193 232
194 class StatsHandler(webapp2.RequestHandler): 233 class StatsHandler(webapp2.RequestHandler):
195 """Returns the statistics web page.""" 234 """Returns the statistics web page."""
196 def get(self): 235 def get(self):
197 """Presents nice recent statistics. 236 """Presents nice recent statistics.
198 237
199 It fetches data from the 'JSON' API. 238 It fetches data from the 'JSON' API.
200 """ 239 """
201 # Preloads the data to save a complete request. 240 # Preloads the data to save a complete request.
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after
291 # Administrative urls. 330 # Administrative urls.
292 webapp2.Route(r'/restricted/config', RestrictedConfigHandler), 331 webapp2.Route(r'/restricted/config', RestrictedConfigHandler),
293 332
294 # Mapreduce related urls. 333 # Mapreduce related urls.
295 webapp2.Route( 334 webapp2.Route(
296 r'/restricted/launch_mapreduce', 335 r'/restricted/launch_mapreduce',
297 RestrictedLaunchMapReduceJob), 336 RestrictedLaunchMapReduceJob),
298 337
299 # User web pages. 338 # User web pages.
300 webapp2.Route(r'/browse', BrowseHandler), 339 webapp2.Route(r'/browse', BrowseHandler),
340 webapp2.Route(r'/content', ContentHandler),
301 webapp2.Route(r'/stats', StatsHandler), 341 webapp2.Route(r'/stats', StatsHandler),
302 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler), 342 webapp2.Route(r'/isolate/api/v1/stats/days', StatsGvizDaysHandler),
303 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler), 343 webapp2.Route(r'/isolate/api/v1/stats/hours', StatsGvizHoursHandler),
304 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler), 344 webapp2.Route(r'/isolate/api/v1/stats/minutes', StatsGvizMinutesHandler),
305 webapp2.Route(r'/', RootHandler), 345 webapp2.Route(r'/', RootHandler),
306 346
307 # AppEngine-specific urls: 347 # AppEngine-specific urls:
308 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler), 348 webapp2.Route(r'/_ah/mail/<to:.+>', EmailHandler),
309 webapp2.Route(r'/_ah/warmup', WarmupHandler), 349 webapp2.Route(r'/_ah/warmup', WarmupHandler),
310 ] 350 ]
311 351
312 352
313 def create_application(debug): 353 def create_application(debug):
314 """Creates the url router. 354 """Creates the url router.
315 355
316 The basic layouts is as follow: 356 The basic layouts is as follow:
317 - /restricted/.* requires being an instance administrator. 357 - /restricted/.* requires being an instance administrator.
318 - /stats/.* has statistics. 358 - /stats/.* has statistics.
319 """ 359 """
320 acl.bootstrap() 360 acl.bootstrap()
321 template.bootstrap() 361 template.bootstrap()
322 return webapp2.WSGIApplication(get_routes(), debug=debug) 362 return webapp2.WSGIApplication(get_routes(), debug=debug)
OLDNEW
« no previous file with comments | « no previous file | appengine/isolate/templates/browse.html » ('j') | appengine/isolate/templates/browse.html » ('J')

Powered by Google App Engine
This is Rietveld 408576698