##// END OF EJS Templates
move signature checking to base NotebookManager...
MinRK -
Show More
@@ -1,425 +1,418
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import io
21 21 import itertools
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from tornado import web
27 27
28 28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current, sign
29 from IPython.nbformat import current
30 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 31 from IPython.utils import tz
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Classes
35 35 #-----------------------------------------------------------------------------
36 36
37 37 class FileNotebookManager(NotebookManager):
38 38
39 39 save_script = Bool(False, config=True,
40 40 help="""Automatically create a Python script when saving the notebook.
41 41
42 42 For easier use of import, %run and %load across notebooks, a
43 43 <notebook-name>.py script will be created next to any
44 44 <notebook-name>.ipynb on each save. This can also be set with the
45 45 short `--script` flag.
46 46 """
47 47 )
48 48
49 49 checkpoint_dir = Unicode(config=True,
50 50 help="""The location in which to keep notebook checkpoints
51 51
52 52 By default, it is notebook-dir/.ipynb_checkpoints
53 53 """
54 54 )
55 55 def _checkpoint_dir_default(self):
56 56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 57
58 58 def _checkpoint_dir_changed(self, name, old, new):
59 59 """do a bit of validation of the checkpoint dir"""
60 60 if not os.path.isabs(new):
61 61 # If we receive a non-absolute path, make it absolute.
62 62 abs_new = os.path.abspath(new)
63 63 self.checkpoint_dir = abs_new
64 64 return
65 65 if os.path.exists(new) and not os.path.isdir(new):
66 66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 67 if not os.path.exists(new):
68 68 self.log.info("Creating checkpoint dir %s", new)
69 69 try:
70 70 os.mkdir(new)
71 71 except:
72 72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 73
74 74 def get_notebook_names(self, path=''):
75 75 """List all notebook names in the notebook dir and path."""
76 76 path = path.strip('/')
77 77 if not os.path.isdir(self.get_os_path(path=path)):
78 78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 80 names = [os.path.basename(name)
81 81 for name in names]
82 82 return names
83 83
84 84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 85 """Return a non-used filename of the form basename<int>."""
86 86 path = path.strip('/')
87 87 for i in itertools.count():
88 88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 89 os_path = self.get_os_path(name, path)
90 90 if not os.path.isfile(os_path):
91 91 break
92 92 return name
93 93
94 94 def path_exists(self, path):
95 95 """Does the API-style path (directory) actually exist?
96 96
97 97 Parameters
98 98 ----------
99 99 path : string
100 100 The path to check. This is an API path (`/` separated,
101 101 relative to base notebook-dir).
102 102
103 103 Returns
104 104 -------
105 105 exists : bool
106 106 Whether the path is indeed a directory.
107 107 """
108 108 path = path.strip('/')
109 109 os_path = self.get_os_path(path=path)
110 110 return os.path.isdir(os_path)
111 111
112 112 def get_os_path(self, name=None, path=''):
113 113 """Given a notebook name and a URL path, return its file system
114 114 path.
115 115
116 116 Parameters
117 117 ----------
118 118 name : string
119 119 The name of a notebook file with the .ipynb extension
120 120 path : string
121 121 The relative URL path (with '/' as separator) to the named
122 122 notebook.
123 123
124 124 Returns
125 125 -------
126 126 path : string
127 127 A file system path that combines notebook_dir (location where
128 128 server started), the relative path, and the filename with the
129 129 current operating system's url.
130 130 """
131 131 parts = path.strip('/').split('/')
132 132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 133 if name is not None:
134 134 parts.append(name)
135 135 path = os.path.join(self.notebook_dir, *parts)
136 136 return path
137 137
138 138 def notebook_exists(self, name, path=''):
139 139 """Returns a True if the notebook exists. Else, returns False.
140 140
141 141 Parameters
142 142 ----------
143 143 name : string
144 144 The name of the notebook you are checking.
145 145 path : string
146 146 The relative path to the notebook (with '/' as separator)
147 147
148 148 Returns
149 149 -------
150 150 bool
151 151 """
152 152 path = path.strip('/')
153 153 nbpath = self.get_os_path(name, path=path)
154 154 return os.path.isfile(nbpath)
155 155
156 156 def list_notebooks(self, path):
157 157 """Returns a list of dictionaries that are the standard model
158 158 for all notebooks in the relative 'path'.
159 159
160 160 Parameters
161 161 ----------
162 162 path : str
163 163 the URL path that describes the relative path for the
164 164 listed notebooks
165 165
166 166 Returns
167 167 -------
168 168 notebooks : list of dicts
169 169 a list of the notebook models without 'content'
170 170 """
171 171 path = path.strip('/')
172 172 notebook_names = self.get_notebook_names(path)
173 173 notebooks = []
174 174 for name in notebook_names:
175 175 model = self.get_notebook_model(name, path, content=False)
176 176 notebooks.append(model)
177 177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 178 return notebooks
179 179
180 180 def get_notebook_model(self, name, path='', content=True):
181 181 """ Takes a path and name for a notebook and returns its model
182 182
183 183 Parameters
184 184 ----------
185 185 name : str
186 186 the name of the notebook
187 187 path : str
188 188 the URL path that describes the relative path for
189 189 the notebook
190 190
191 191 Returns
192 192 -------
193 193 model : dict
194 194 the notebook model. If contents=True, returns the 'contents'
195 195 dict in the model as well.
196 196 """
197 197 path = path.strip('/')
198 198 if not self.notebook_exists(name=name, path=path):
199 199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 200 os_path = self.get_os_path(name, path)
201 201 info = os.stat(os_path)
202 202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 203 created = tz.utcfromtimestamp(info.st_ctime)
204 204 # Create the notebook model.
205 205 model ={}
206 206 model['name'] = name
207 207 model['path'] = path
208 208 model['last_modified'] = last_modified
209 209 model['created'] = created
210 210 if content:
211 211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 212 try:
213 213 nb = current.read(f, u'json')
214 214 except Exception as e:
215 215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 self.mark_trusted_cells(nb, path, name)
216 217 model['content'] = nb
217 trusted = self.notary.check_signature(nb)
218 if not trusted:
219 self.log.warn("Notebook %s/%s is not trusted", model['path'], model['name'])
220 self.notary.mark_cells(nb, trusted)
221 218 return model
222 219
223 220 def save_notebook_model(self, model, name='', path=''):
224 221 """Save the notebook model and return the model with no content."""
225 222 path = path.strip('/')
226 223
227 224 if 'content' not in model:
228 225 raise web.HTTPError(400, u'No notebook JSON data provided')
229 226
230 227 # One checkpoint should always exist
231 228 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
232 229 self.create_checkpoint(name, path)
233 230
234 231 new_path = model.get('path', path).strip('/')
235 232 new_name = model.get('name', name)
236 233
237 234 if path != new_path or name != new_name:
238 235 self.rename_notebook(name, path, new_name, new_path)
239 236
240 237 # Save the notebook file
241 238 os_path = self.get_os_path(new_name, new_path)
242 239 nb = current.to_notebook_json(model['content'])
243 240
244 if self.notary.check_cells(nb):
245 self.notary.sign(nb)
246 else:
247 self.log.warn("Saving untrusted notebook %s/%s", new_path, new_name)
248
241 self.check_and_sign(nb, new_path, new_name)
249 242
250 243 if 'name' in nb['metadata']:
251 244 nb['metadata']['name'] = u''
252 245 try:
253 246 self.log.debug("Autosaving notebook %s", os_path)
254 247 with io.open(os_path, 'w', encoding='utf-8') as f:
255 248 current.write(nb, f, u'json')
256 249 except Exception as e:
257 250 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
258 251
259 252 # Save .py script as well
260 253 if self.save_script:
261 254 py_path = os.path.splitext(os_path)[0] + '.py'
262 255 self.log.debug("Writing script %s", py_path)
263 256 try:
264 257 with io.open(py_path, 'w', encoding='utf-8') as f:
265 258 current.write(nb, f, u'py')
266 259 except Exception as e:
267 260 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
268 261
269 262 model = self.get_notebook_model(new_name, new_path, content=False)
270 263 return model
271 264
272 265 def update_notebook_model(self, model, name, path=''):
273 266 """Update the notebook's path and/or name"""
274 267 path = path.strip('/')
275 268 new_name = model.get('name', name)
276 269 new_path = model.get('path', path).strip('/')
277 270 if path != new_path or name != new_name:
278 271 self.rename_notebook(name, path, new_name, new_path)
279 272 model = self.get_notebook_model(new_name, new_path, content=False)
280 273 return model
281 274
282 275 def delete_notebook_model(self, name, path=''):
283 276 """Delete notebook by name and path."""
284 277 path = path.strip('/')
285 278 os_path = self.get_os_path(name, path)
286 279 if not os.path.isfile(os_path):
287 280 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
288 281
289 282 # clear checkpoints
290 283 for checkpoint in self.list_checkpoints(name, path):
291 284 checkpoint_id = checkpoint['id']
292 285 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
293 286 if os.path.isfile(cp_path):
294 287 self.log.debug("Unlinking checkpoint %s", cp_path)
295 288 os.unlink(cp_path)
296 289
297 290 self.log.debug("Unlinking notebook %s", os_path)
298 291 os.unlink(os_path)
299 292
300 293 def rename_notebook(self, old_name, old_path, new_name, new_path):
301 294 """Rename a notebook."""
302 295 old_path = old_path.strip('/')
303 296 new_path = new_path.strip('/')
304 297 if new_name == old_name and new_path == old_path:
305 298 return
306 299
307 300 new_os_path = self.get_os_path(new_name, new_path)
308 301 old_os_path = self.get_os_path(old_name, old_path)
309 302
310 303 # Should we proceed with the move?
311 304 if os.path.isfile(new_os_path):
312 305 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
313 306 if self.save_script:
314 307 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
315 308 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
316 309 if os.path.isfile(new_py_path):
317 310 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
318 311
319 312 # Move the notebook file
320 313 try:
321 314 os.rename(old_os_path, new_os_path)
322 315 except Exception as e:
323 316 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
324 317
325 318 # Move the checkpoints
326 319 old_checkpoints = self.list_checkpoints(old_name, old_path)
327 320 for cp in old_checkpoints:
328 321 checkpoint_id = cp['id']
329 322 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
330 323 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
331 324 if os.path.isfile(old_cp_path):
332 325 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
333 326 os.rename(old_cp_path, new_cp_path)
334 327
335 328 # Move the .py script
336 329 if self.save_script:
337 330 os.rename(old_py_path, new_py_path)
338 331
339 332 # Checkpoint-related utilities
340 333
341 334 def get_checkpoint_path(self, checkpoint_id, name, path=''):
342 335 """find the path to a checkpoint"""
343 336 path = path.strip('/')
344 337 basename, _ = os.path.splitext(name)
345 338 filename = u"{name}-{checkpoint_id}{ext}".format(
346 339 name=basename,
347 340 checkpoint_id=checkpoint_id,
348 341 ext=self.filename_ext,
349 342 )
350 343 cp_path = os.path.join(path, self.checkpoint_dir, filename)
351 344 return cp_path
352 345
353 346 def get_checkpoint_model(self, checkpoint_id, name, path=''):
354 347 """construct the info dict for a given checkpoint"""
355 348 path = path.strip('/')
356 349 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
357 350 stats = os.stat(cp_path)
358 351 last_modified = tz.utcfromtimestamp(stats.st_mtime)
359 352 info = dict(
360 353 id = checkpoint_id,
361 354 last_modified = last_modified,
362 355 )
363 356 return info
364 357
365 358 # public checkpoint API
366 359
367 360 def create_checkpoint(self, name, path=''):
368 361 """Create a checkpoint from the current state of a notebook"""
369 362 path = path.strip('/')
370 363 nb_path = self.get_os_path(name, path)
371 364 # only the one checkpoint ID:
372 365 checkpoint_id = u"checkpoint"
373 366 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
374 367 self.log.debug("creating checkpoint for notebook %s", name)
375 368 if not os.path.exists(self.checkpoint_dir):
376 369 os.mkdir(self.checkpoint_dir)
377 370 shutil.copy2(nb_path, cp_path)
378 371
379 372 # return the checkpoint info
380 373 return self.get_checkpoint_model(checkpoint_id, name, path)
381 374
382 375 def list_checkpoints(self, name, path=''):
383 376 """list the checkpoints for a given notebook
384 377
385 378 This notebook manager currently only supports one checkpoint per notebook.
386 379 """
387 380 path = path.strip('/')
388 381 checkpoint_id = "checkpoint"
389 382 path = self.get_checkpoint_path(checkpoint_id, name, path)
390 383 if not os.path.exists(path):
391 384 return []
392 385 else:
393 386 return [self.get_checkpoint_model(checkpoint_id, name, path)]
394 387
395 388
396 389 def restore_checkpoint(self, checkpoint_id, name, path=''):
397 390 """restore a notebook to a checkpointed state"""
398 391 path = path.strip('/')
399 392 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
400 393 nb_path = self.get_os_path(name, path)
401 394 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
402 395 if not os.path.isfile(cp_path):
403 396 self.log.debug("checkpoint file does not exist: %s", cp_path)
404 397 raise web.HTTPError(404,
405 398 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
406 399 )
407 400 # ensure notebook is readable (never restore from an unreadable notebook)
408 401 with io.open(cp_path, 'r', encoding='utf-8') as f:
409 402 nb = current.read(f, u'json')
410 403 shutil.copy2(cp_path, nb_path)
411 404 self.log.debug("copying %s -> %s", cp_path, nb_path)
412 405
413 406 def delete_checkpoint(self, checkpoint_id, name, path=''):
414 407 """delete a notebook's checkpoint"""
415 408 path = path.strip('/')
416 409 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
417 410 if not os.path.isfile(cp_path):
418 411 raise web.HTTPError(404,
419 412 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
420 413 )
421 414 self.log.debug("unlinking %s", cp_path)
422 415 os.unlink(cp_path)
423 416
424 417 def info_string(self):
425 418 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,178 +1,198
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import os
21 21
22 22 from IPython.config.configurable import LoggingConfigurable
23 23 from IPython.nbformat import current, sign
24 24 from IPython.utils import py3compat
25 25 from IPython.utils.traitlets import Instance, Unicode, TraitError
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class NotebookManager(LoggingConfigurable):
32 32
33 33 # Todo:
34 34 # The notebook_dir attribute is used to mean a couple of different things:
35 35 # 1. Where the notebooks are stored if FileNotebookManager is used.
36 36 # 2. The cwd of the kernel for a project.
37 37 # Right now we use this attribute in a number of different places and
38 38 # we are going to have to disentangle all of this.
39 39 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
40 40 The directory to use for notebooks.
41 41 """)
42 42
43 43 filename_ext = Unicode(u'.ipynb')
44 44
45 45 notary = Instance(sign.NotebookNotary)
46 46 def _notary_default(self):
47 47 return sign.NotebookNotary(parent=self)
48 48
49 def check_and_sign(self, nb, path, name):
50 """Check for trusted cells, and sign the notebook.
51
52 Called as a part of saving notebooks.
53 """
54 if self.notary.check_cells(nb):
55 self.notary.sign(nb)
56 else:
57 self.log.warn("Saving untrusted notebook %s/%s", path, name)
58
59 def mark_trusted_cells(self, nb, path, name):
60 """Mark cells as trusted if the notebook signature matches.
61
62 Called as a part of loading notebooks.
63 """
64 trusted = self.notary.check_signature(nb)
65 if not trusted:
66 self.log.warn("Notebook %s/%s is not trusted", path, name)
67 self.notary.mark_cells(nb, trusted)
68
49 69 def path_exists(self, path):
50 70 """Does the API-style path (directory) actually exist?
51 71
52 72 Override this method in subclasses.
53 73
54 74 Parameters
55 75 ----------
56 76 path : string
57 77 The
58 78
59 79 Returns
60 80 -------
61 81 exists : bool
62 82 Whether the path does indeed exist.
63 83 """
64 84 raise NotImplementedError
65 85
66 86 def _notebook_dir_changed(self, name, old, new):
67 87 """Do a bit of validation of the notebook dir."""
68 88 if not os.path.isabs(new):
69 89 # If we receive a non-absolute path, make it absolute.
70 90 self.notebook_dir = os.path.abspath(new)
71 91 return
72 92 if os.path.exists(new) and not os.path.isdir(new):
73 93 raise TraitError("notebook dir %r is not a directory" % new)
74 94 if not os.path.exists(new):
75 95 self.log.info("Creating notebook dir %s", new)
76 96 try:
77 97 os.mkdir(new)
78 98 except:
79 99 raise TraitError("Couldn't create notebook dir %r" % new)
80 100
81 101 # Main notebook API
82 102
83 103 def increment_filename(self, basename, path=''):
84 104 """Increment a notebook filename without the .ipynb to make it unique.
85 105
86 106 Parameters
87 107 ----------
88 108 basename : unicode
89 109 The name of a notebook without the ``.ipynb`` file extension.
90 110 path : unicode
91 111 The URL path of the notebooks directory
92 112 """
93 113 return basename
94 114
95 115 def list_notebooks(self, path=''):
96 116 """Return a list of notebook dicts without content.
97 117
98 118 This returns a list of dicts, each of the form::
99 119
100 120 dict(notebook_id=notebook,name=name)
101 121
102 122 This list of dicts should be sorted by name::
103 123
104 124 data = sorted(data, key=lambda item: item['name'])
105 125 """
106 126 raise NotImplementedError('must be implemented in a subclass')
107 127
108 128 def get_notebook_model(self, name, path='', content=True):
109 129 """Get the notebook model with or without content."""
110 130 raise NotImplementedError('must be implemented in a subclass')
111 131
112 132 def save_notebook_model(self, model, name, path=''):
113 133 """Save the notebook model and return the model with no content."""
114 134 raise NotImplementedError('must be implemented in a subclass')
115 135
116 136 def update_notebook_model(self, model, name, path=''):
117 137 """Update the notebook model and return the model with no content."""
118 138 raise NotImplementedError('must be implemented in a subclass')
119 139
120 140 def delete_notebook_model(self, name, path=''):
121 141 """Delete notebook by name and path."""
122 142 raise NotImplementedError('must be implemented in a subclass')
123 143
124 144 def create_notebook_model(self, model=None, path=''):
125 145 """Create a new notebook and return its model with no content."""
126 146 path = path.strip('/')
127 147 if model is None:
128 148 model = {}
129 149 if 'content' not in model:
130 150 metadata = current.new_metadata(name=u'')
131 151 model['content'] = current.new_notebook(metadata=metadata)
132 152 if 'name' not in model:
133 153 model['name'] = self.increment_filename('Untitled', path)
134 154
135 155 model['path'] = path
136 156 model = self.save_notebook_model(model, model['name'], model['path'])
137 157 return model
138 158
139 159 def copy_notebook(self, from_name, to_name=None, path=''):
140 160 """Copy an existing notebook and return its new model.
141 161
142 162 If to_name not specified, increment `from_name-Copy#.ipynb`.
143 163 """
144 164 path = path.strip('/')
145 165 model = self.get_notebook_model(from_name, path)
146 166 if not to_name:
147 167 base = os.path.splitext(from_name)[0] + '-Copy'
148 168 to_name = self.increment_filename(base, path)
149 169 model['name'] = to_name
150 170 model = self.save_notebook_model(model, to_name, path)
151 171 return model
152 172
153 173 # Checkpoint-related
154 174
155 175 def create_checkpoint(self, name, path=''):
156 176 """Create a checkpoint of the current state of a notebook
157 177
158 178 Returns a checkpoint_id for the new checkpoint.
159 179 """
160 180 raise NotImplementedError("must be implemented in a subclass")
161 181
162 182 def list_checkpoints(self, name, path=''):
163 183 """Return a list of checkpoints for a given notebook"""
164 184 return []
165 185
166 186 def restore_checkpoint(self, checkpoint_id, name, path=''):
167 187 """Restore a notebook from one of its checkpoints"""
168 188 raise NotImplementedError("must be implemented in a subclass")
169 189
170 190 def delete_checkpoint(self, checkpoint_id, name, path=''):
171 191 """delete a checkpoint for a notebook"""
172 192 raise NotImplementedError("must be implemented in a subclass")
173 193
174 194 def log_info(self):
175 195 self.log.info(self.info_string())
176 196
177 197 def info_string(self):
178 198 return "Serving notebooks"
General Comments 0
You need to be logged in to leave comments. Login now