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
|
# Imports
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
import logging
|
|
||||||
import Cookie
|
import Cookie
|
||||||
|
import datetime
|
||||||
|
import email.utils
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -31,6 +38,7 @@ from IPython.external.decorator import decorator
|
|||||||
from IPython.zmq.session import Session
|
from IPython.zmq.session import Session
|
||||||
from IPython.lib.security import passwd_check
|
from IPython.lib.security import passwd_check
|
||||||
from IPython.utils.jsonutil import date_default
|
from IPython.utils.jsonutil import date_default
|
||||||
|
from IPython.utils.path import filefind
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from docutils.core import publish_string
|
from docutils.core import publish_string
|
||||||
@ -735,4 +743,178 @@ class RSTHandler(AuthenticatedHandler):
|
|||||||
self.set_header('Content-Type', 'text/html')
|
self.set_header('Content-Type', 'text/html')
|
||||||
self.finish(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,
|
MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
|
||||||
ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
|
ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
|
||||||
RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
|
RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
|
||||||
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
|
MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
|
||||||
|
FileFindHandler,
|
||||||
)
|
)
|
||||||
from .notebookmanager import NotebookManager
|
from .notebookmanager import NotebookManager
|
||||||
from .clustermanager import ClusterManager
|
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.traitlets import Dict, Unicode, Integer, List, Enum, Bool
|
||||||
from IPython.utils import py3compat
|
from IPython.utils import py3compat
|
||||||
|
from IPython.utils.path import filefind
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Module globals
|
# Module globals
|
||||||
@ -153,7 +155,8 @@ class NotebookWebApplication(web.Application):
|
|||||||
|
|
||||||
settings = dict(
|
settings = dict(
|
||||||
template_path=os.path.join(os.path.dirname(__file__), "templates"),
|
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),
|
cookie_secret=os.urandom(1024),
|
||||||
login_url="%s/login"%(base_project_url.rstrip('/')),
|
login_url="%s/login"%(base_project_url.rstrip('/')),
|
||||||
)
|
)
|
||||||
@ -356,19 +359,31 @@ class NotebookApp(BaseIPythonApplication):
|
|||||||
help="""The hostname for the websocket server."""
|
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,
|
mathjax_url = Unicode("", config=True,
|
||||||
help="""The url for MathJax.js."""
|
help="""The url for MathJax.js."""
|
||||||
)
|
)
|
||||||
def _mathjax_url_default(self):
|
def _mathjax_url_default(self):
|
||||||
if not self.enable_mathjax:
|
if not self.enable_mathjax:
|
||||||
return u''
|
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_url_prefix = self.webapp_settings.get("static_url_prefix",
|
||||||
"/static/")
|
"/static/")
|
||||||
if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
|
try:
|
||||||
self.log.info("Using local MathJax")
|
mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
|
||||||
return static_url_prefix+u"mathjax/MathJax.js"
|
except IOError:
|
||||||
else:
|
|
||||||
if self.certfile:
|
if self.certfile:
|
||||||
# HTTPS: load from Rackspace CDN, because SSL certificate requires it
|
# HTTPS: load from Rackspace CDN, because SSL certificate requires it
|
||||||
base = u"https://c328740.ssl.cf1.rackcdn.com"
|
base = u"https://c328740.ssl.cf1.rackcdn.com"
|
||||||
@ -378,6 +393,9 @@ class NotebookApp(BaseIPythonApplication):
|
|||||||
url = base + u"/mathjax/latest/MathJax.js"
|
url = base + u"/mathjax/latest/MathJax.js"
|
||||||
self.log.info("Using MathJax from CDN: %s", url)
|
self.log.info("Using MathJax from CDN: %s", url)
|
||||||
return 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):
|
def _mathjax_url_changed(self, name, old, new):
|
||||||
if new and not self.enable_mathjax:
|
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/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/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 %}
|
{% end %}
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
<link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
|
<link rel="stylesheet" href="{{static_url("css/page.css") }}" type="text/css"/>
|
||||||
{% block stylesheet %}
|
{% block stylesheet %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
<link rel="stylesheet" href="{{ static_url("css/custom.css") }}" type="text/css" />
|
||||||
|
|
||||||
|
|
||||||
{% block meta %}
|
{% block meta %}
|
||||||
{% end %}
|
{% end %}
|
||||||
@ -53,6 +55,8 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
<script src="{{static_url("js/custom.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user