diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index d9b36c3b0..4cb674b25 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -22,7 +22,6 @@ import json import logging import os import re -import stat import sys import traceback try: @@ -42,10 +41,7 @@ except ImportError: from IPython.config import Application from IPython.utils.path import filefind from IPython.utils.py3compat import string_types - -# UF_HIDDEN is a stat flag not defined in the stat module. -# It is used by BSD to indicate hidden files. -UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) +from IPython.html.utils import is_hidden #----------------------------------------------------------------------------- # Top-level handlers @@ -269,28 +265,9 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler): """ abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path) abs_root = os.path.abspath(root) - self.forbid_hidden(abs_root, abs_path) + if is_hidden(abs_path, abs_root): + raise web.HTTPError(404) return abs_path - - def forbid_hidden(self, absolute_root, absolute_path): - """Raise 403 if a file is hidden or contained in a hidden directory. - - Hidden is determined by either name starting with '.' - or the UF_HIDDEN flag as reported by stat - """ - inside_root = absolute_path[len(absolute_root):] - if any(part.startswith('.') for part in inside_root.split(os.sep)): - raise web.HTTPError(403) - - # check UF_HIDDEN on any location up to root - path = absolute_path - while path and path.startswith(absolute_root) and path != absolute_root: - st = os.stat(path) - if getattr(st, 'st_flags', 0) & UF_HIDDEN: - raise web.HTTPError(403) - path = os.path.dirname(path) - - return absolute_path def json_errors(method): diff --git a/IPython/html/services/notebooks/filenbmanager.py b/IPython/html/services/notebooks/filenbmanager.py index bac4de245..33606be22 100644 --- a/IPython/html/services/notebooks/filenbmanager.py +++ b/IPython/html/services/notebooks/filenbmanager.py @@ -29,6 +29,7 @@ from .nbmanager import NotebookManager from IPython.nbformat import current from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError from IPython.utils import tz +from IPython.html.utils import is_hidden #----------------------------------------------------------------------------- # Classes @@ -108,7 +109,26 @@ class FileNotebookManager(NotebookManager): path = path.strip('/') os_path = self.get_os_path(path=path) return os.path.isdir(os_path) - + + def is_hidden(self, path): + """Does the API style path correspond to a hidden directory or file? + + Parameters + ---------- + path : string + The path to check. This is an API path (`/` separated, + relative to base notebook-dir). + + Returns + ------- + exists : bool + Whether the path is hidden. + + """ + path = path.strip('/') + os_path = self.get_os_path(path=path) + return is_hidden(os_path, self.notebook_dir) + def get_os_path(self, name=None, path=''): """Given a notebook name and a URL path, return its file system path. @@ -153,6 +173,47 @@ class FileNotebookManager(NotebookManager): nbpath = self.get_os_path(name, path=path) return os.path.isfile(nbpath) + # TODO: Remove this after we create the contents web service and directories are + # no longer listed by the notebook web service. + def list_dirs(self, path): + """List the directories for a given API style path.""" + path = path.strip('/') + os_path = self.get_os_path('', path) + if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir): + raise web.HTTPError(404, u'directory does not exist: %r' % os_path) + dir_names = os.listdir(os_path) + dirs = [] + for name in dir_names: + os_path = self.get_os_path(name, path) + if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir): + try: + model = self.get_dir_model(name, path) + except IOError: + pass + dirs.append(model) + dirs = sorted(dirs, key=lambda item: item['name']) + return dirs + + # TODO: Remove this after we create the contents web service and directories are + # no longer listed by the notebook web service. + def get_dir_model(self, name, path=''): + """Get the directory model given a directory name and its API style path""" + path = path.strip('/') + os_path = self.get_os_path(name, path) + if not os.path.isdir(os_path): + raise IOError('directory does not exist: %r' % os_path) + info = os.stat(os_path) + last_modified = tz.utcfromtimestamp(info.st_mtime) + created = tz.utcfromtimestamp(info.st_ctime) + # Create the notebook model. + model ={} + model['name'] = name + model['path'] = path + model['last_modified'] = last_modified + model['created'] = created + model['type'] = 'directory' + return model + def list_notebooks(self, path): """Returns a list of dictionaries that are the standard model for all notebooks in the relative 'path'. @@ -170,10 +231,7 @@ class FileNotebookManager(NotebookManager): """ path = path.strip('/') notebook_names = self.get_notebook_names(path) - notebooks = [] - for name in notebook_names: - model = self.get_notebook_model(name, path, content=False) - notebooks.append(model) + notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names] notebooks = sorted(notebooks, key=lambda item: item['name']) return notebooks @@ -207,6 +265,7 @@ class FileNotebookManager(NotebookManager): model['path'] = path model['last_modified'] = last_modified model['created'] = created + model['type'] = 'notebook' if content: with io.open(os_path, 'r', encoding='utf-8') as f: try: @@ -223,7 +282,7 @@ class FileNotebookManager(NotebookManager): if 'content' not in model: raise web.HTTPError(400, u'No notebook JSON data provided') - + # One checkpoint should always exist if self.notebook_exists(name, path) and not self.list_checkpoints(name, path): self.create_checkpoint(name, path) diff --git a/IPython/html/services/notebooks/handlers.py b/IPython/html/services/notebooks/handlers.py index 27e74d76b..ac44dd659 100644 --- a/IPython/html/services/notebooks/handlers.py +++ b/IPython/html/services/notebooks/handlers.py @@ -69,8 +69,18 @@ class NotebookHandler(IPythonHandler): nbm = self.notebook_manager # Check to see if a notebook name was given if name is None: - # List notebooks in 'path' - notebooks = nbm.list_notebooks(path) + # TODO: Remove this after we create the contents web service and directories are + # no longer listed by the notebook web service. This should only handle notebooks + # and not directories. + dirs = nbm.list_dirs(path) + notebooks = [] + index = [] + for nb in nbm.list_notebooks(path): + if nb['name'].lower() == 'index.ipynb': + index.append(nb) + else: + notebooks.append(nb) + notebooks = index + dirs + notebooks self.finish(json.dumps(notebooks, default=date_default)) return # get and return notebook representation diff --git a/IPython/html/services/notebooks/nbmanager.py b/IPython/html/services/notebooks/nbmanager.py index f86e914e3..80b3ce5a3 100644 --- a/IPython/html/services/notebooks/nbmanager.py +++ b/IPython/html/services/notebooks/nbmanager.py @@ -82,7 +82,24 @@ class NotebookManager(LoggingConfigurable): Whether the path does indeed exist. """ raise NotImplementedError - + + def is_hidden(self, path): + """Does the API style path correspond to a hidden directory or file? + + Parameters + ---------- + path : string + The path to check. This is an API path (`/` separated, + relative to base notebook-dir). + + Returns + ------- + exists : bool + Whether the path is hidden. + + """ + raise NotImplementedError + def _notebook_dir_changed(self, name, old, new): """Do a bit of validation of the notebook dir.""" if not os.path.isabs(new): @@ -112,6 +129,26 @@ class NotebookManager(LoggingConfigurable): """ return basename + # TODO: Remove this after we create the contents web service and directories are + # no longer listed by the notebook web service. + def list_dirs(self, path): + """List the directory models for a given API style path.""" + raise NotImplementedError('must be implemented in a subclass') + + # TODO: Remove this after we create the contents web service and directories are + # no longer listed by the notebook web service. + def get_dir_model(self, name, path=''): + """Get the directory model given a directory name and its API style path. + + The keys in the model should be: + * name + * path + * last_modified + * created + * type='directory' + """ + raise NotImplementedError('must be implemented in a subclass') + def list_notebooks(self, path=''): """Return a list of notebook dicts without content. diff --git a/IPython/html/services/notebooks/tests/test_nbmanager.py b/IPython/html/services/notebooks/tests/test_nbmanager.py index 8d0adfae1..b6a240ecd 100644 --- a/IPython/html/services/notebooks/tests/test_nbmanager.py +++ b/IPython/html/services/notebooks/tests/test_nbmanager.py @@ -15,6 +15,7 @@ from IPython.html.utils import url_path_join from ..filenbmanager import FileNotebookManager from ..nbmanager import NotebookManager + class TestFileNotebookManager(TestCase): def test_nb_dir(self): @@ -67,7 +68,7 @@ class TestNotebookManager(TestCase): try: os.makedirs(os_path) except OSError: - print("Directory already exists.") + print("Directory already exists: %r" % os_path) def test_create_notebook_model(self): with TemporaryDirectory() as td: diff --git a/IPython/html/services/notebooks/tests/test_notebooks_api.py b/IPython/html/services/notebooks/tests/test_notebooks_api.py index 4e6e04de2..5ac0cb1f8 100644 --- a/IPython/html/services/notebooks/tests/test_notebooks_api.py +++ b/IPython/html/services/notebooks/tests/test_notebooks_api.py @@ -21,6 +21,12 @@ from IPython.utils import py3compat from IPython.utils.data import uniq_stable +# TODO: Remove this after we create the contents web service and directories are +# no longer listed by the notebook web service. +def notebooks_only(nb_list): + return [nb for nb in nb_list if nb['type']=='notebook'] + + class NBAPI(object): """Wrapper for notebook API calls.""" def __init__(self, base_url): @@ -125,25 +131,25 @@ class APITest(NotebookTestBase): os.unlink(pjoin(nbdir, 'inroot.ipynb')) def test_list_notebooks(self): - nbs = self.nb_api.list().json() + nbs = notebooks_only(self.nb_api.list().json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'inroot.ipynb') - nbs = self.nb_api.list('/Directory with spaces in/').json() + nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'inspace.ipynb') - nbs = self.nb_api.list(u'/unicodé/').json() + nbs = notebooks_only(self.nb_api.list(u'/unicodé/').json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'innonascii.ipynb') self.assertEqual(nbs[0]['path'], u'unicodé') - nbs = self.nb_api.list('/foo/bar/').json() + nbs = notebooks_only(self.nb_api.list('/foo/bar/').json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'baz.ipynb') self.assertEqual(nbs[0]['path'], 'foo/bar') - nbs = self.nb_api.list('foo').json() + nbs = notebooks_only(self.nb_api.list('foo').json()) self.assertEqual(len(nbs), 4) nbnames = { normalize('NFC', n['name']) for n in nbs } expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb'] @@ -231,7 +237,7 @@ class APITest(NotebookTestBase): self.assertEqual(resp.status_code, 204) for d in self.dirs + ['/']: - nbs = self.nb_api.list(d).json() + nbs = notebooks_only(self.nb_api.list(d).json()) self.assertEqual(len(nbs), 0) def test_rename(self): @@ -240,7 +246,7 @@ class APITest(NotebookTestBase): self.assertEqual(resp.json()['name'], 'z.ipynb') assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb')) - nbs = self.nb_api.list('foo').json() + nbs = notebooks_only(self.nb_api.list('foo').json()) nbnames = set(n['name'] for n in nbs) self.assertIn('z.ipynb', nbnames) self.assertNotIn('a.ipynb', nbnames) diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css index c1af57105..7325b41b9 100644 --- a/IPython/html/static/style/ipython.min.css +++ b/IPython/html/static/style/ipython.min.css @@ -28,22 +28,29 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto} .center-nav{display:inline-block;margin-bottom:-4px} .alternate_upload{background-color:none;display:inline} .alternate_upload.form{padding:0;margin:0} -.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer} -.list_toolbar{padding:5px;height:25px;line-height:25px} -.toolbar_info{float:left} -.toolbar_buttons{float:right} +.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px} +ul#tabs{margin-bottom:4px} +ul#tabs a{padding-top:4px;padding-bottom:4px} +ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none} +ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px} +ul.breadcrumb span{color:#5e5e5e} +.list_toolbar{padding:4px 0 4px 0} +.list_toolbar [class*="span"]{min-height:26px} .list_header{font-weight:bold} -.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px} +.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px} .list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00} .list_container>div:last-child{border:none} .list_item:hover .list_item{background-color:#ddd} -.item_name{line-height:24px} -.list_container>div>span,.list_container>div>div{padding:8px} .list_item a{text-decoration:none} -input.nbname_input{height:15px} +.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px} +.item_name{line-height:22px;height:26px} +.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px} +.item_buttons{line-height:1em} +.toolbar_info{height:26px;line-height:26px} +input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0} +input.engine_num_input{width:60px} .highlight_text{color:#00f} #project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold} -input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px} .ansibold{font-weight:bold} .ansiblack{color:#000} .ansired{color:#8b0000} diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css index b301b7b25..62fc8f494 100644 --- a/IPython/html/static/style/style.min.css +++ b/IPython/html/static/style/style.min.css @@ -1305,22 +1305,29 @@ span#login_widget{float:right} .center-nav{display:inline-block;margin-bottom:-4px} .alternate_upload{background-color:none;display:inline} .alternate_upload.form{padding:0;margin:0} -.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer} -.list_toolbar{padding:5px;height:25px;line-height:25px} -.toolbar_info{float:left} -.toolbar_buttons{float:right} +.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px} +ul#tabs{margin-bottom:4px} +ul#tabs a{padding-top:4px;padding-bottom:4px} +ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none} +ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px} +ul.breadcrumb span{color:#5e5e5e} +.list_toolbar{padding:4px 0 4px 0} +.list_toolbar [class*="span"]{min-height:26px} .list_header{font-weight:bold} -.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px} +.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px} .list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00} .list_container>div:last-child{border:none} .list_item:hover .list_item{background-color:#ddd} -.item_name{line-height:24px} -.list_container>div>span,.list_container>div>div{padding:8px} .list_item a{text-decoration:none} -input.nbname_input{height:15px} +.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px} +.item_name{line-height:22px;height:26px} +.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px} +.item_buttons{line-height:1em} +.toolbar_info{height:26px;line-height:26px} +input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0} +input.engine_num_input{width:60px} .highlight_text{color:#00f} #project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold} -input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px} .ansibold{font-weight:bold} .ansiblack{color:#000} .ansired{color:#8b0000} diff --git a/IPython/html/static/tree/js/clusterlist.js b/IPython/html/static/tree/js/clusterlist.js index 2ece25d8e..9d607d50b 100644 --- a/IPython/html/static/tree/js/clusterlist.js +++ b/IPython/html/static/tree/js/clusterlist.js @@ -100,16 +100,16 @@ var IPython = (function (IPython) { ClusterItem.prototype.state_stopped = function () { var that = this; - var profile_col = $('').addClass('profile_col span4').text(this.data.profile); - var status_col = $('').addClass('status_col span3').text('stopped'); - var engines_col = $('').addClass('engine_col span3'); + var profile_col = $('
').addClass('profile_col span4').text(this.data.profile); + var status_col = $('').addClass('status_col span3').text('stopped'); + var engines_col = $('').addClass('engine_col span3'); var input = $('').attr('type','number') .attr('min',1) .attr('size',3) .addClass('engine_num_input'); engines_col.append(input); var start_button = $('').addClass("btn btn-mini").text("Start"); - var action_col = $('').addClass('action_col span2').append( + var action_col = $('').addClass('action_col span2').append( $("").addClass("item_buttons btn-group").append( start_button ) @@ -151,11 +151,11 @@ var IPython = (function (IPython) { ClusterItem.prototype.state_running = function () { var that = this; - var profile_col = $('').addClass('profile_col span4').text(this.data.profile); - var status_col = $('').addClass('status_col span3').text('running'); - var engines_col = $('').addClass('engines_col span3').text(this.data.n); + var profile_col = $('').addClass('profile_col span4').text(this.data.profile); + var status_col = $('').addClass('status_col span3').text('running'); + var engines_col = $('').addClass('engines_col span3').text(this.data.n); var stop_button = $('').addClass("btn btn-mini").text("Stop"); - var action_col = $('').addClass('action_col span2').append( + var action_col = $('').addClass('action_col span2').append( $("").addClass("item_buttons btn-group").append( stop_button ) diff --git a/IPython/html/static/tree/js/notebooklist.js b/IPython/html/static/tree/js/notebooklist.js index cc2f7fba8..e92c34386 100644 --- a/IPython/html/static/tree/js/notebooklist.js +++ b/IPython/html/static/tree/js/notebooklist.js @@ -70,11 +70,10 @@ var IPython = (function (IPython) { var reader = new FileReader(); reader.readAsText(f); var name_and_ext = utils.splitext(f.name); - var nbname = name_and_ext[0]; var file_ext = name_and_ext[1]; if (file_ext === '.ipynb') { var item = that.new_notebook_item(0); - that.add_name_input(nbname, item); + that.add_name_input(f.name, item); // Store the notebook item in the reader so we can use it later // to know which item it belongs to. $(reader).data('item', item); @@ -167,26 +166,37 @@ var IPython = (function (IPython) { if (param !== undefined && param.msg) { message = param.msg; } + var item = null; var len = data.length; this.clear_list(); if (len === 0) { - $(this.new_notebook_item(0)) - .append( - $('') - .text(message) - ); + item = this.new_notebook_item(0); + var span12 = item.children().first(); + span12.empty(); + span12.append($('').text(message)); + } + var path = this.notebookPath(); + var offset = 0; + if (path !== '') { + item = this.new_notebook_item(0); + this.add_dir(path, '..', item); + offset = 1; } for (var i=0; i