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

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: Add comment 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', '')
154 content = None 155 params = {
156 u'digest': unicode(digest),
157 u'namespace': unicode(namespace),
158 }
159 # Check for existence of element, so we can 400/404
155 if digest and namespace: 160 if digest and namespace:
156 # TODO(maruel): Refactor into a function. 161 try:
157 memcache_entry = memcache.get(digest, namespace='table_%s' % namespace) 162 model.get_content(namespace, digest)
158 if memcache_entry is not None: 163 except ValueError:
159 raw_data = memcache_entry 164 self.abort(400, 'Invalid key')
160 else: 165 except LookupError:
161 try: 166 self.abort(404, 'Unable to retrieve the entry')
162 key = model.get_entry_key(namespace, digest) 167 self.response.write(template.render('isolate/browse.html', params))
163 except ValueError: 168
164 self.abort(400, 'Invalid key') 169
165 entity = key.get() 170 class ContentHandler(auth.AuthenticatingHandler):
166 if entity is None: 171 @auth.autologin
167 self.abort(404, 'Unable to retrieve the entry') 172 @auth.require(acl.isolate_readable)
168 raw_data = entity.content 173 def get(self):
174 namespace = self.request.get('namespace', 'default-gzip')
175 digest = self.request.get('digest', '')
176
177 raw_data = None
M-A Ruel 2016/04/15 19:54:10 You confused this, it's only content that needs to
178 entity = None
179 if digest and namespace:
180 try:
181 raw_data, entity = model.get_content(namespace, digest)
182 except ValueError:
183 self.abort(400, 'Invalid key')
184 except LookupError:
185 self.abort(404, 'Unable to retrieve the entry')
186
169 if not raw_data: 187 if not raw_data:
170 stream = gcs.read_file(config.settings().gs_bucket, key.id()) 188 stream = gcs.read_file(config.settings().gs_bucket, entity.key.id())
171 else: 189 else:
172 stream = [raw_data] 190 stream = [raw_data]
173 content = ''.join(model.expand_content(namespace, stream)) 191 content = ''.join(model.expand_content(namespace, stream))
192
193 self.response.headers['X-Frame-Options'] = 'SAMEORIGIN'
194 # We delete Content-Type before storing to it to avoid having two (yes,
195 # two) Content-Type headers.
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
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=%s' % namespace +
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') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698