diff --git a/IPython/frontend/html/notebook/handlers.py b/IPython/frontend/html/notebook/handlers.py
index c4a755125..60613802b 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
+
+