Show More
@@ -16,6 +16,16 b' Authors:' | |||||
16 | # Imports |
|
16 | # Imports | |
17 | #----------------------------------------------------------------------------- |
|
17 | #----------------------------------------------------------------------------- | |
18 |
|
18 | |||
|
19 | ||||
|
20 | import datetime | |||
|
21 | import email.utils | |||
|
22 | import hashlib | |||
|
23 | import logging | |||
|
24 | import mimetypes | |||
|
25 | import os | |||
|
26 | import stat | |||
|
27 | import threading | |||
|
28 | ||||
19 | import logging |
|
29 | import logging | |
20 |
|
30 | |||
21 | from tornado import web |
|
31 | from tornado import web | |
@@ -28,6 +38,7 b' except ImportError:' | |||||
28 |
|
38 | |||
29 | from IPython.config import Application |
|
39 | from IPython.config import Application | |
30 | from IPython.external.decorator import decorator |
|
40 | from IPython.external.decorator import decorator | |
|
41 | from IPython.utils.path import filefind | |||
31 |
|
42 | |||
32 | #----------------------------------------------------------------------------- |
|
43 | #----------------------------------------------------------------------------- | |
33 | # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! |
|
44 | # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! | |
@@ -277,6 +288,165 b' class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):' | |||||
277 |
|
288 | |||
278 |
|
289 | |||
279 | #----------------------------------------------------------------------------- |
|
290 | #----------------------------------------------------------------------------- | |
|
291 | # File handler | |||
|
292 | #----------------------------------------------------------------------------- | |||
|
293 | ||||
|
294 | # to minimize subclass changes: | |||
|
295 | HTTPError = web.HTTPError | |||
|
296 | ||||
|
297 | class FileFindHandler(web.StaticFileHandler): | |||
|
298 | """subclass of StaticFileHandler for serving files from a search path""" | |||
|
299 | ||||
|
300 | _static_paths = {} | |||
|
301 | # _lock is needed for tornado < 2.2.0 compat | |||
|
302 | _lock = threading.Lock() # protects _static_hashes | |||
|
303 | ||||
|
304 | def initialize(self, path, default_filename=None): | |||
|
305 | if isinstance(path, basestring): | |||
|
306 | path = [path] | |||
|
307 | self.roots = tuple( | |||
|
308 | os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path | |||
|
309 | ) | |||
|
310 | self.default_filename = default_filename | |||
|
311 | ||||
|
312 | @classmethod | |||
|
313 | def locate_file(cls, path, roots): | |||
|
314 | """locate a file to serve on our static file search path""" | |||
|
315 | with cls._lock: | |||
|
316 | if path in cls._static_paths: | |||
|
317 | return cls._static_paths[path] | |||
|
318 | try: | |||
|
319 | abspath = os.path.abspath(filefind(path, roots)) | |||
|
320 | except IOError: | |||
|
321 | # empty string should always give exists=False | |||
|
322 | return '' | |||
|
323 | ||||
|
324 | # os.path.abspath strips a trailing / | |||
|
325 | # it needs to be temporarily added back for requests to root/ | |||
|
326 | if not (abspath + os.path.sep).startswith(roots): | |||
|
327 | raise HTTPError(403, "%s is not in root static directory", path) | |||
|
328 | ||||
|
329 | cls._static_paths[path] = abspath | |||
|
330 | return abspath | |||
|
331 | ||||
|
332 | def get(self, path, include_body=True): | |||
|
333 | path = self.parse_url_path(path) | |||
|
334 | ||||
|
335 | # begin subclass override | |||
|
336 | abspath = self.locate_file(path, self.roots) | |||
|
337 | # end subclass override | |||
|
338 | ||||
|
339 | if os.path.isdir(abspath) and self.default_filename is not None: | |||
|
340 | # need to look at the request.path here for when path is empty | |||
|
341 | # but there is some prefix to the path that was already | |||
|
342 | # trimmed by the routing | |||
|
343 | if not self.request.path.endswith("/"): | |||
|
344 | self.redirect(self.request.path + "/") | |||
|
345 | return | |||
|
346 | abspath = os.path.join(abspath, self.default_filename) | |||
|
347 | if not os.path.exists(abspath): | |||
|
348 | raise HTTPError(404) | |||
|
349 | if not os.path.isfile(abspath): | |||
|
350 | raise HTTPError(403, "%s is not a file", path) | |||
|
351 | ||||
|
352 | stat_result = os.stat(abspath) | |||
|
353 | modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME]) | |||
|
354 | ||||
|
355 | self.set_header("Last-Modified", modified) | |||
|
356 | ||||
|
357 | mime_type, encoding = mimetypes.guess_type(abspath) | |||
|
358 | if mime_type: | |||
|
359 | self.set_header("Content-Type", mime_type) | |||
|
360 | ||||
|
361 | cache_time = self.get_cache_time(path, modified, mime_type) | |||
|
362 | ||||
|
363 | if cache_time > 0: | |||
|
364 | self.set_header("Expires", datetime.datetime.utcnow() + \ | |||
|
365 | datetime.timedelta(seconds=cache_time)) | |||
|
366 | self.set_header("Cache-Control", "max-age=" + str(cache_time)) | |||
|
367 | else: | |||
|
368 | self.set_header("Cache-Control", "public") | |||
|
369 | ||||
|
370 | self.set_extra_headers(path) | |||
|
371 | ||||
|
372 | # Check the If-Modified-Since, and don't send the result if the | |||
|
373 | # content has not been modified | |||
|
374 | ims_value = self.request.headers.get("If-Modified-Since") | |||
|
375 | if ims_value is not None: | |||
|
376 | date_tuple = email.utils.parsedate(ims_value) | |||
|
377 | if_since = datetime.datetime(*date_tuple[:6]) | |||
|
378 | if if_since >= modified: | |||
|
379 | self.set_status(304) | |||
|
380 | return | |||
|
381 | ||||
|
382 | with open(abspath, "rb") as file: | |||
|
383 | data = file.read() | |||
|
384 | hasher = hashlib.sha1() | |||
|
385 | hasher.update(data) | |||
|
386 | self.set_header("Etag", '"%s"' % hasher.hexdigest()) | |||
|
387 | if include_body: | |||
|
388 | self.write(data) | |||
|
389 | else: | |||
|
390 | assert self.request.method == "HEAD" | |||
|
391 | self.set_header("Content-Length", len(data)) | |||
|
392 | ||||
|
393 | @classmethod | |||
|
394 | def get_version(cls, settings, path): | |||
|
395 | """Generate the version string to be used in static URLs. | |||
|
396 | ||||
|
397 | This method may be overridden in subclasses (but note that it | |||
|
398 | is a class method rather than a static method). The default | |||
|
399 | implementation uses a hash of the file's contents. | |||
|
400 | ||||
|
401 | ``settings`` is the `Application.settings` dictionary and ``path`` | |||
|
402 | is the relative location of the requested asset on the filesystem. | |||
|
403 | The returned value should be a string, or ``None`` if no version | |||
|
404 | could be determined. | |||
|
405 | """ | |||
|
406 | # begin subclass override: | |||
|
407 | static_paths = settings['static_path'] | |||
|
408 | if isinstance(static_paths, basestring): | |||
|
409 | static_paths = [static_paths] | |||
|
410 | roots = tuple( | |||
|
411 | os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths | |||
|
412 | ) | |||
|
413 | ||||
|
414 | try: | |||
|
415 | abs_path = filefind(path, roots) | |||
|
416 | except IOError: | |||
|
417 | app_log.error("Could not find static file %r", path) | |||
|
418 | return None | |||
|
419 | ||||
|
420 | # end subclass override | |||
|
421 | ||||
|
422 | with cls._lock: | |||
|
423 | hashes = cls._static_hashes | |||
|
424 | if abs_path not in hashes: | |||
|
425 | try: | |||
|
426 | f = open(abs_path, "rb") | |||
|
427 | hashes[abs_path] = hashlib.md5(f.read()).hexdigest() | |||
|
428 | f.close() | |||
|
429 | except Exception: | |||
|
430 | app_log.error("Could not open static file %r", path) | |||
|
431 | hashes[abs_path] = None | |||
|
432 | hsh = hashes.get(abs_path) | |||
|
433 | if hsh: | |||
|
434 | return hsh[:5] | |||
|
435 | return None | |||
|
436 | ||||
|
437 | ||||
|
438 | def parse_url_path(self, url_path): | |||
|
439 | """Converts a static URL path into a filesystem path. | |||
|
440 | ||||
|
441 | ``url_path`` is the path component of the URL with | |||
|
442 | ``static_url_prefix`` removed. The return value should be | |||
|
443 | filesystem path relative to ``static_path``. | |||
|
444 | """ | |||
|
445 | if os.path.sep != "/": | |||
|
446 | url_path = url_path.replace("/", os.path.sep) | |||
|
447 | return url_path | |||
|
448 | ||||
|
449 | #----------------------------------------------------------------------------- | |||
280 | # URL to handler mappings |
|
450 | # URL to handler mappings | |
281 | #----------------------------------------------------------------------------- |
|
451 | #----------------------------------------------------------------------------- | |
282 |
|
452 |
1 | NO CONTENT: file renamed from IPython/frontend/html/notebook/clusters/handlers.py to IPython/frontend/html/notebook/clusters/apihandlers.py |
|
NO CONTENT: file renamed from IPython/frontend/html/notebook/clusters/handlers.py to IPython/frontend/html/notebook/clusters/apihandlers.py |
1 | NO CONTENT: file renamed from IPython/frontend/html/notebook/kernels/handlers.py to IPython/frontend/html/notebook/kernels/apihandlers.py |
|
NO CONTENT: file renamed from IPython/frontend/html/notebook/kernels/handlers.py to IPython/frontend/html/notebook/kernels/apihandlers.py |
@@ -68,8 +68,7 b' from .notebooks.nbmanager import NotebookManager' | |||||
68 | from .notebooks.filenbmanager import FileNotebookManager |
|
68 | from .notebooks.filenbmanager import FileNotebookManager | |
69 | from .clusters.clustermanager import ClusterManager |
|
69 | from .clusters.clustermanager import ClusterManager | |
70 |
|
70 | |||
71 | from .base.handlers import AuthenticatedFileHandler |
|
71 | from .base.handlers import AuthenticatedFileHandler, FileFindHandler | |
72 | from .base.files import FileFindHandler |
|
|||
73 |
|
72 | |||
74 | from IPython.config.application import catch_config_error, boolean_flag |
|
73 | from IPython.config.application import catch_config_error, boolean_flag | |
75 | from IPython.core.application import BaseIPythonApplication |
|
74 | from IPython.core.application import BaseIPythonApplication | |
@@ -142,9 +141,9 b' class NotebookWebApplication(web.Application):' | |||||
142 | handlers.extend(load_handlers('auth.login')) |
|
141 | handlers.extend(load_handlers('auth.login')) | |
143 | handlers.extend(load_handlers('auth.logout')) |
|
142 | handlers.extend(load_handlers('auth.logout')) | |
144 | handlers.extend(load_handlers('notebooks.handlers')) |
|
143 | handlers.extend(load_handlers('notebooks.handlers')) | |
145 | handlers.extend(load_handlers('kernels.handlers')) |
|
144 | handlers.extend(load_handlers('kernels.apihandlers')) | |
146 | handlers.extend(load_handlers('notebooks.apihandlers')) |
|
145 | handlers.extend(load_handlers('notebooks.apihandlers')) | |
147 | handlers.extend(load_handlers('clusters.handlers')) |
|
146 | handlers.extend(load_handlers('clusters.apihandlers')) | |
148 | handlers.extend([ |
|
147 | handlers.extend([ | |
149 | (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}), |
|
148 | (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}), | |
150 | ]) |
|
149 | ]) |
1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now