diff --git a/IPython/frontend/html/notebook/filenbmanager.py b/IPython/frontend/html/notebook/filenbmanager.py
index 832f9fb..e83420f 100644
--- a/IPython/frontend/html/notebook/filenbmanager.py
+++ b/IPython/frontend/html/notebook/filenbmanager.py
@@ -20,6 +20,7 @@ import datetime
import io
import os
import glob
+import shutil
from tornado import web
@@ -43,11 +44,36 @@ class FileNotebookManager(NotebookManager):
"""
)
+ checkpoint_dir = Unicode(config=True,
+ help="""The location in which to keep notebook checkpoints
+
+ By default, it is notebook-dir/.ipynb_checkpoints
+ """
+ )
+ def _checkpoint_dir_default(self):
+ return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
+
+ def _checkpoint_dir_changed(self, name, old, new):
+ """do a bit of validation of the checkpoint dir"""
+ if not os.path.isabs(new):
+ # If we receive a non-absolute path, make it absolute.
+ abs_new = os.path.abspath(new)
+ self.checkpoint_dir = abs_new
+ return
+ if os.path.exists(new) and not os.path.isdir(new):
+ raise TraitError("checkpoint dir %r is not a directory" % new)
+ if not os.path.exists(new):
+ self.log.info("Creating checkpoint dir %s", new)
+ try:
+ os.mkdir(new)
+ except:
+ raise TraitError("Couldn't create checkpoint dir %r" % new)
+
filename_ext = Unicode(u'.ipynb')
# Map notebook names to notebook_ids
rev_mapping = Dict()
-
+
def get_notebook_names(self):
"""List all notebook names in the notebook dir."""
names = glob.glob(os.path.join(self.notebook_dir,
@@ -89,26 +115,28 @@ class FileNotebookManager(NotebookManager):
return False
path = self.get_path_by_name(self.mapping[notebook_id])
return os.path.isfile(path)
-
- def find_path(self, notebook_id):
- """Return a full path to a notebook given its notebook_id."""
+
+ def get_name(self, notebook_id):
+ """get a notebook name, raising 404 if not found"""
try:
name = self.mapping[notebook_id]
except KeyError:
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ return name
+
+ def get_path(self, notebook_id):
+ """Return a full path to a notebook given its notebook_id."""
+ name = self.get_name(notebook_id)
return self.get_path_by_name(name)
def get_path_by_name(self, name):
"""Return a full path to a notebook given its name."""
filename = name + self.filename_ext
path = os.path.join(self.notebook_dir, filename)
- return path
+ return path
- def read_notebook_object(self, notebook_id):
- """Get the NotebookNode representation of a notebook by notebook_id."""
- path = self.find_path(notebook_id)
- if not os.path.isfile(path):
- raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ def read_notebook_object_from_path(self, path):
+ """read a notebook object from a path"""
info = os.stat(path)
last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
with open(path,'r') as f:
@@ -116,12 +144,20 @@ class FileNotebookManager(NotebookManager):
try:
# v1 and v2 and json in the .ipynb files.
nb = current.reads(s, u'json')
- except:
- raise web.HTTPError(500, u'Unreadable JSON notebook.')
+ except Exception as e:
+ raise web.HTTPError(500, u'Unreadable JSON notebook: %s' % e)
+ return last_modified, nb
+
+ def read_notebook_object(self, notebook_id):
+ """Get the Notebook representation of a notebook by notebook_id."""
+ path = self.get_path(notebook_id)
+ if not os.path.isfile(path):
+ raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ last_modified, nb = self.read_notebook_object_from_path(path)
# Always use the filename as the notebook name.
nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
return last_modified, nb
-
+
def write_notebook_object(self, nb, notebook_id=None):
"""Save an existing notebook object by notebook_id."""
try:
@@ -136,8 +172,11 @@ class FileNotebookManager(NotebookManager):
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
old_name = self.mapping[notebook_id]
+ old_checkpoints = self.list_checkpoints(notebook_id)
+
path = self.get_path_by_name(new_name)
try:
+ self.log.debug("Writing notebook %s", path)
with open(path,'w') as f:
current.write(nb, f, u'json')
except Exception as e:
@@ -146,6 +185,7 @@ class FileNotebookManager(NotebookManager):
# save .py script as well
if self.save_script:
pypath = os.path.splitext(path)[0] + '.py'
+ self.log.debug("Writing script %s", pypath)
try:
with io.open(pypath,'w', encoding='utf-8') as f:
current.write(nb, f, u'py')
@@ -154,25 +194,49 @@ class FileNotebookManager(NotebookManager):
# remove old files if the name changed
if old_name != new_name:
+ # update mapping
+ self.mapping[notebook_id] = new_name
+ self.rev_mapping[new_name] = notebook_id
+ del self.rev_mapping[old_name]
+
+ # remove renamed original, if it exists
old_path = self.get_path_by_name(old_name)
if os.path.isfile(old_path):
+ self.log.debug("unlinking %s", old_path)
os.unlink(old_path)
+
+ # cleanup old script, if it exists
if self.save_script:
old_pypath = os.path.splitext(old_path)[0] + '.py'
if os.path.isfile(old_pypath):
+ self.log.debug("unlinking %s", old_pypath)
os.unlink(old_pypath)
- self.mapping[notebook_id] = new_name
- self.rev_mapping[new_name] = notebook_id
- del self.rev_mapping[old_name]
-
+
+ # rename checkpoints to follow file
+ self.log.debug("%s", old_checkpoints)
+ for cp in old_checkpoints:
+ old_cp_path = self.get_checkpoint_path_by_name(old_name, cp)
+ new_cp_path = self.get_checkpoint_path_by_name(new_name, cp)
+ if os.path.isfile(old_cp_path):
+ self.log.debug("renaming %s -> %s", old_cp_path, new_cp_path)
+ os.rename(old_cp_path, new_cp_path)
+
return notebook_id
def delete_notebook(self, notebook_id):
"""Delete notebook by notebook_id."""
- path = self.find_path(notebook_id)
+ path = self.get_path(notebook_id)
if not os.path.isfile(path):
raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
+ self.log.debug("unlinking %s", path)
os.unlink(path)
+
+ # clear checkpoints
+ for checkpoint_id in self.list_checkpoints(notebook_id):
+ path = self.get_checkpoint_path(notebook_id, checkpoint_id)
+ if os.path.isfile(path):
+ self.log.debug("unlinking %s", path)
+ os.unlink(path)
self.delete_notebook_id(notebook_id)
def increment_filename(self, basename):
@@ -191,6 +255,69 @@ class FileNotebookManager(NotebookManager):
else:
i = i+1
return name
+
+ # Checkpoint-related utilities
+
+ def get_checkpoint_path_by_name(self, name, checkpoint_id):
+ """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
+ filename = "{name}-{checkpoint_id}{ext}".format(
+ name=name,
+ checkpoint_id=checkpoint_id,
+ ext=self.filename_ext,
+ )
+ path = os.path.join(self.checkpoint_dir, filename)
+ return path
+
+ def get_checkpoint_path(self, notebook_id, checkpoint_id):
+ """find the path to a checkpoint"""
+ name = self.get_name(notebook_id)
+ return self.get_checkpoint_path_by_name(name, checkpoint_id)
+
+ # public checkpoint API
+
+ def create_checkpoint(self, notebook_id):
+ """Create a checkpoint from the current state of a notebook"""
+ nb_path = self.get_path(notebook_id)
+ cp_path = self.get_checkpoint_path(notebook_id, "checkpoint")
+ self.log.debug("creating checkpoint for notebook %s", notebook_id)
+ if not os.path.exists(self.checkpoint_dir):
+ os.mkdir(self.checkpoint_dir)
+ shutil.copy2(nb_path, cp_path)
+
+ def list_checkpoints(self, notebook_id):
+ """list the checkpoints for a given notebook
+ This notebook manager currently only supports one checkpoint per notebook.
+ """
+ path = self.get_checkpoint_path(notebook_id, "checkpoint")
+ if os.path.exists(path):
+ return ["checkpoint"]
+ else:
+ return []
+
+ def restore_checkpoint(self, notebook_id, checkpoint_id):
+ """restore a notebook to a checkpointed state"""
+ self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
+ nb_path = self.get_path(notebook_id)
+ cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
+ if not os.path.isfile(cp_path):
+ raise web.HTTPError(404,
+ u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
+ )
+ # ensure notebook is readable (never restore from an unreadable notebook)
+ last_modified, nb = self.read_notebook_object_from_path(cp_path)
+ shutil.copy2(cp_path, nb_path)
+ self.log.debug("copying %s -> %s", cp_path, nb_path)
+
+ def delete_checkpoint(self, notebook_id, checkpoint_id):
+ """delete a notebook's checkpoint"""
+ path = self.get_checkpoint_path(notebook_id, checkpoint_id)
+ if not os.path.isfile(path):
+ raise web.HTTPError(404,
+ u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
+ )
+ self.log.debug("unlinking %s", path)
+ os.unlink(path)
+
def info_string(self):
return "Serving notebooks from local directory: %s" % self.notebook_dir
diff --git a/IPython/frontend/html/notebook/nbmanager.py b/IPython/frontend/html/notebook/nbmanager.py
index e7a1dbf..0a9321f 100644
--- a/IPython/frontend/html/notebook/nbmanager.py
+++ b/IPython/frontend/html/notebook/nbmanager.py
@@ -36,7 +36,7 @@ class NotebookManager(LoggingConfigurable):
# 1. Where the notebooks are stored if FileNotebookManager is used.
# 2. The cwd of the kernel for a project.
# Right now we use this attribute in a number of different places and
- # we are going to have to disentagle all of this.
+ # we are going to have to disentangle all of this.
notebook_dir = Unicode(os.getcwdu(), config=True, help="""
The directory to use for notebooks.
""")
@@ -205,7 +205,28 @@ class NotebookManager(LoggingConfigurable):
nb.metadata.name = name
notebook_id = self.write_notebook_object(nb)
return notebook_id
+
+ # Checkpoint-related
+
+ def create_checkpoint(self, notebook_id):
+ """Create a checkpoint of the current state of a notebook
+
+ Returns a checkpoint_id for the new checkpoint.
+ """
+ raise NotImplementedError("must be implemented in a subclass")
+
+ def list_checkpoints(self, notebook_id):
+ """Return a list of checkpoints for a given notebook"""
+ return []
+
+ def restore_checkpoint(self, notebook_id, checkpoint_id):
+ """Restore a notebook from one of its checkpoints"""
+ raise NotImplementedError("must be implemented in a subclass")
+ def delete_checkpoint(self, notebook_id, checkpoint_id):
+ """delete a checkpoint for a notebook"""
+ raise NotImplementedError("must be implemented in a subclass")
+
def log_info(self):
self.log.info(self.info_string())