diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py index c4a7551..6061380 100644 --- a/IPython/frontend/html/notebook/handlers.py +++ b/IPython/frontend/html/notebook/handlers.py @@ -16,8 +16,14 @@ Authors: # Imports #----------------------------------------------------------------------------- -import logging import Cookie +import datetime +import email.utils +import hashlib +import logging +import mimetypes +import os +import stat import time import uuid @@ -31,6 +37,7 @@ from IPython.external.decorator import decorator from IPython.zmq.session import Session from IPython.lib.security import passwd_check from IPython.utils.jsonutil import date_default +from IPython.utils.path import filefind try: from docutils.core import publish_string @@ -736,3 +743,138 @@ class RSTHandler(AuthenticatedHandler): self.finish(html) +class FileFindHandler(web.StaticFileHandler): + """subclass of StaticFileHandler for serving files from a search path""" + + _static_paths = {} + + def initialize(self, path, default_filename=None): + self.roots = tuple( + os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path + ) + self.default_filename = default_filename + + @classmethod + def locate_file(cls, path, roots): + """locate a file to serve on our static file search path""" + with cls._lock: + if path in cls._static_paths: + return cls._static_paths[path] + try: + abspath = os.path.abspath(filefind(path, roots)) + except IOError: + # empty string should always give exists=False + return '' + + # os.path.abspath strips a trailing / + # it needs to be temporarily added back for requests to root/ + if not (abspath + os.path.sep).startswith(roots): + raise web.HTTPError(403, "%s is not in root static directory", path) + + cls._static_paths[path] = abspath + return abspath + + def get(self, path, include_body=True): + path = self.parse_url_path(path) + abspath = self.locate_file(path, self.roots) + + # from here on, this method is unchanged from the parent: + # other than using web.HTTPError instead of just HTTPError + + if os.path.isdir(abspath) and self.default_filename is not None: + # need to look at the request.path here for when path is empty + # but there is some prefix to the path that was already + # trimmed by the routing + if not self.request.path.endswith("/"): + self.redirect(self.request.path + "/") + return + abspath = os.path.join(abspath, self.default_filename) + if not os.path.exists(abspath): + raise web.HTTPError(404) + if not os.path.isfile(abspath): + raise web.HTTPError(403, "%s is not a file", path) + + stat_result = os.stat(abspath) + modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME]) + + self.set_header("Last-Modified", modified) + + mime_type, encoding = mimetypes.guess_type(abspath) + if mime_type: + self.set_header("Content-Type", mime_type) + + cache_time = self.get_cache_time(path, modified, mime_type) + + if cache_time > 0: + self.set_header("Expires", datetime.datetime.utcnow() + \ + datetime.timedelta(seconds=cache_time)) + self.set_header("Cache-Control", "max-age=" + str(cache_time)) + else: + self.set_header("Cache-Control", "public") + + self.set_extra_headers(path) + + # Check the If-Modified-Since, and don't send the result if the + # content has not been modified + ims_value = self.request.headers.get("If-Modified-Since") + if ims_value is not None: + date_tuple = email.utils.parsedate(ims_value) + if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)) + if if_since >= modified: + self.set_status(304) + return + + with open(abspath, "rb") as file: + data = file.read() + hasher = hashlib.sha1() + hasher.update(data) + self.set_header("Etag", '"%s"' % hasher.hexdigest()) + if include_body: + self.write(data) + else: + assert self.request.method == "HEAD" + self.set_header("Content-Length", len(data)) + + @classmethod + def get_version(cls, settings, path): + """Generate the version string to be used in static URLs. + + This method may be overridden in subclasses (but note that it + is a class method rather than a static method). The default + implementation uses a hash of the file's contents. + + ``settings`` is the `Application.settings` dictionary and ``path`` + is the relative location of the requested asset on the filesystem. + The returned value should be a string, or ``None`` if no version + could be determined. + """ + # begin subclass override: + static_path = settings['static_path'] + roots = tuple( + os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_path + ) + + try: + abs_path = filefind(path, roots) + except Exception: + logging.error("Could not find static file %r", path) + return None + + # end subclass override + + with cls._lock: + hashes = cls._static_hashes + if abs_path not in hashes: + try: + f = open(abs_path, "rb") + hashes[abs_path] = hashlib.md5(f.read()).hexdigest() + f.close() + except Exception: + logging.error("Could not open static file %r", path) + hashes[abs_path] = None + hsh = hashes.get(abs_path) + if hsh: + return hsh[:5] + return None + +