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

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

Powered by Google App Engine
This is Rietveld 408576698