mirror of
https://github.com/jupyter/notebook.git
synced 2024-12-27 04:20:22 +08:00
Merge pull request #2175 from minrk/staticfile
add FileFindHandler for Notebook static files * The static file handler now uses a search path, instead of a single dir. This allows easier customization of available js/css, and provides a place for extra files to go for extending the notebook. * An empty custom.js / custom.css are added to the templates for trivial custom user styling/scripting. The search only happens once, and the result is cached after the first. * A few methods are pulled from tornado 2.2-dev verbatim to have tornado 2.1 compatibility. * mathjax is now installed by default in profile.
This commit is contained in:
commit
9a52bdc18b
@ -16,8 +16,15 @@ Authors:
|
||||
# Imports
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import logging
|
||||
import Cookie
|
||||
import datetime
|
||||
import email.utils
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
@ -31,6 +38,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
|
||||
@ -735,4 +743,178 @@ class RSTHandler(AuthenticatedHandler):
|
||||
self.set_header('Content-Type', 'text/html')
|
||||
self.finish(html)
|
||||
|
||||
# to minimize subclass changes:
|
||||
HTTPError = web.HTTPError
|
||||
|
||||
class FileFindHandler(web.StaticFileHandler):
|
||||
"""subclass of StaticFileHandler for serving files from a search path"""
|
||||
|
||||
_static_paths = {}
|
||||
# _lock is needed for tornado < 2.2.0 compat
|
||||
_lock = threading.Lock() # protects _static_hashes
|
||||
|
||||
def initialize(self, path, default_filename=None):
|
||||
if isinstance(path, basestring):
|
||||
path = [path]
|
||||
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 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)
|
||||
|
||||
# begin subclass override
|
||||
abspath = self.locate_file(path, self.roots)
|
||||
# end subclass override
|
||||
|
||||
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 HTTPError(404)
|
||||
if not os.path.isfile(abspath):
|
||||
raise 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_paths = settings['static_path']
|
||||
if isinstance(static_paths, basestring):
|
||||
static_paths = [static_paths]
|
||||
roots = tuple(
|
||||
os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
|
||||
)
|
||||
|
||||
try:
|
||||
abs_path = filefind(path, roots)
|
||||
except IOError:
|
||||
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
|
||||
|
||||
|
||||
# make_static_url and parse_url_path totally unchanged from tornado 2.2.0
|
||||
# but needed for tornado < 2.2.0 compat
|
||||
@classmethod
|
||||
def make_static_url(cls, settings, path):
|
||||
"""Constructs a versioned url for the given path.
|
||||
|
||||
This method may be overridden in subclasses (but note that it is
|
||||
a class method rather than an instance method).
|
||||
|
||||
``settings`` is the `Application.settings` dictionary. ``path``
|
||||
is the static path being requested. The url returned should be
|
||||
relative to the current host.
|
||||
"""
|
||||
static_url_prefix = settings.get('static_url_prefix', '/static/')
|
||||
version_hash = cls.get_version(settings, path)
|
||||
if version_hash:
|
||||
return static_url_prefix + path + "?v=" + version_hash
|
||||
return static_url_prefix + path
|
||||
|
||||
def parse_url_path(self, url_path):
|
||||
"""Converts a static URL path into a filesystem path.
|
||||
|
||||
``url_path`` is the path component of the URL with
|
||||
``static_url_prefix`` removed. The return value should be
|
||||
filesystem path relative to ``static_path``.
|
||||
"""
|
||||
if os.path.sep != "/":
|
||||
url_path = url_path.replace("/", os.path.sep)
|
||||
return url_path
|
||||
|
||||
|
||||
|
@ -48,7 +48,8 @@ from .handlers import (LoginHandler, LogoutHandler,
|
||||
MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
|
||||
ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
|
||||
RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
|
||||
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
|
||||
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
|
||||
FileFindHandler,
|
||||
)
|
||||
from .notebookmanager import NotebookManager
|
||||
from .clustermanager import ClusterManager
|
||||
@ -67,6 +68,7 @@ from IPython.zmq.ipkernel import (
|
||||
)
|
||||
from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
|
||||
from IPython.utils import py3compat
|
||||
from IPython.utils.path import filefind
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Module globals
|
||||
@ -153,7 +155,8 @@ class NotebookWebApplication(web.Application):
|
||||
|
||||
settings = dict(
|
||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
||||
static_path=os.path.join(os.path.dirname(__file__), "static"),
|
||||
static_path=ipython_app.static_file_path,
|
||||
static_handler_class = FileFindHandler,
|
||||
cookie_secret=os.urandom(1024),
|
||||
login_url="%s/login"%(base_project_url.rstrip('/')),
|
||||
)
|
||||
@ -356,19 +359,31 @@ class NotebookApp(BaseIPythonApplication):
|
||||
help="""The hostname for the websocket server."""
|
||||
)
|
||||
|
||||
extra_static_paths = List(Unicode, config=True,
|
||||
help="""Extra paths to search for serving static files.
|
||||
|
||||
This allows adding javascript/css to be available from the notebook server machine,
|
||||
or overriding individual files in the IPython"""
|
||||
)
|
||||
def _extra_static_paths_default(self):
|
||||
return [os.path.join(self.profile_dir.location, 'static')]
|
||||
|
||||
@property
|
||||
def static_file_path(self):
|
||||
"""return extra paths + the default location"""
|
||||
return self.extra_static_paths + [os.path.join(os.path.dirname(__file__), "static")]
|
||||
|
||||
mathjax_url = Unicode("", config=True,
|
||||
help="""The url for MathJax.js."""
|
||||
)
|
||||
def _mathjax_url_default(self):
|
||||
if not self.enable_mathjax:
|
||||
return u''
|
||||
static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
|
||||
static_url_prefix = self.webapp_settings.get("static_url_prefix",
|
||||
"/static/")
|
||||
if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
|
||||
self.log.info("Using local MathJax")
|
||||
return static_url_prefix+u"mathjax/MathJax.js"
|
||||
else:
|
||||
try:
|
||||
mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
|
||||
except IOError:
|
||||
if self.certfile:
|
||||
# HTTPS: load from Rackspace CDN, because SSL certificate requires it
|
||||
base = u"https://c328740.ssl.cf1.rackcdn.com"
|
||||
@ -378,6 +393,9 @@ class NotebookApp(BaseIPythonApplication):
|
||||
url = base + u"/mathjax/latest/MathJax.js"
|
||||
self.log.info("Using MathJax from CDN: %s", url)
|
||||
return url
|
||||
else:
|
||||
self.log.info("Using local MathJax from %s" % mathjax)
|
||||
return static_url_prefix+u"mathjax/MathJax.js"
|
||||
|
||||
def _mathjax_url_changed(self, name, old, new):
|
||||
if new and not self.enable_mathjax:
|
||||
|
7
IPython/frontend/html/notebook/static/css/custom.css
Normal file
7
IPython/frontend/html/notebook/static/css/custom.css
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
Placeholder for custom user CSS
|
||||
|
||||
mainly to be overridden in profile/static/css/custom.css
|
||||
|
||||
This will always be an empty file in IPython
|
||||
*/
|
7
IPython/frontend/html/notebook/static/js/custom.js
Normal file
7
IPython/frontend/html/notebook/static/js/custom.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*
|
||||
Placeholder for custom user javascript
|
||||
|
||||
mainly to be overridden in profile/static/js/custom.js
|
||||
|
||||
This will always be an empty file in IPython
|
||||
*/
|
@ -245,6 +245,6 @@ data-notebook-id={{notebook_id}}
|
||||
<script src="{{ static_url("js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<script src="{{ static_url("js/contexthint.js") }} charset="utf-8"></script>
|
||||
<script src="{{ static_url("js/contexthint.js") }}" charset="utf-8"></script>
|
||||
|
||||
{% end %}
|
||||
|
@ -12,6 +12,8 @@
|
||||
<link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
|
||||
{% block stylesheet %}
|
||||
{% end %}
|
||||
<link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
|
||||
|
||||
|
||||
{% block meta %}
|
||||
{% end %}
|
||||
@ -53,6 +55,8 @@
|
||||
{% block script %}
|
||||
{% end %}
|
||||
|
||||
<script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user