##// END OF EJS Templates
`--script` triggers post_save hook with nbconvert
Min RK -
Show More
@@ -1,659 +1,690 b''
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 import errno
8 8 import io
9 9 import os
10 10 import shutil
11 11 from contextlib import contextmanager
12 12 import mimetypes
13 13
14 14 from tornado import web
15 15
16 16 from .manager import ContentsManager
17 17 from IPython import nbformat
18 18 from IPython.utils.io import atomic_writing
19 19 from IPython.utils.importstring import import_item
20 20 from IPython.utils.path import ensure_dir_exists
21 21 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
22 22 from IPython.utils.py3compat import getcwd, str_to_unicode
23 23 from IPython.utils import tz
24 24 from IPython.html.utils import is_hidden, to_os_path, to_api_path
25 25
26 _script_exporter = None
27 def _post_save_script(model, os_path, contents_manager, **kwargs):
28 """convert notebooks to Python script after save with nbconvert
29
30 replaces `ipython notebook --script`
31 """
32 from IPython.nbconvert.exporters.python import PythonExporter
33
34 if model['type'] != 'notebook':
35 return
36 global _script_exporter
37 if _script_exporter is None:
38 _script_exporter = PythonExporter(parent=contents_manager)
39 log = contents_manager.log
40
41 base, ext = os.path.splitext(os_path)
42 py_fname = base + '.py'
43 log.info("Writing %s", py_fname)
44 py, resources = _script_exporter.from_filename(os_path)
45 with io.open(py_fname, 'w', encoding='utf-8') as f:
46 f.write(py)
26 47
27 48 class FileContentsManager(ContentsManager):
28 49
29 50 root_dir = Unicode(config=True)
30 51
31 52 def _root_dir_default(self):
32 53 try:
33 54 return self.parent.notebook_dir
34 55 except AttributeError:
35 56 return getcwd()
36 57
37 58 @contextmanager
38 59 def perm_to_403(self, os_path=''):
39 60 """context manager for turning permission errors into 403"""
40 61 try:
41 62 yield
42 63 except OSError as e:
43 64 if e.errno in {errno.EPERM, errno.EACCES}:
44 65 # make 403 error message without root prefix
45 66 # this may not work perfectly on unicode paths on Python 2,
46 67 # but nobody should be doing that anyway.
47 68 if not os_path:
48 69 os_path = str_to_unicode(e.filename or 'unknown file')
49 70 path = to_api_path(os_path, self.root_dir)
50 71 raise web.HTTPError(403, u'Permission denied: %s' % path)
51 72 else:
52 73 raise
53 74
54 75 @contextmanager
55 76 def open(self, os_path, *args, **kwargs):
56 77 """wrapper around io.open that turns permission errors into 403"""
57 78 with self.perm_to_403(os_path):
58 79 with io.open(os_path, *args, **kwargs) as f:
59 80 yield f
60 81
61 82 @contextmanager
62 83 def atomic_writing(self, os_path, *args, **kwargs):
63 84 """wrapper around atomic_writing that turns permission errors into 403"""
64 85 with self.perm_to_403(os_path):
65 86 with atomic_writing(os_path, *args, **kwargs) as f:
66 87 yield f
67 88
68 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
89 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
69 90 def _save_script_changed(self):
70 91 self.log.warn("""
71 Automatically saving notebooks as scripts has been removed.
72 Use `ipython nbconvert --to python [notebook]` instead.
92 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
93
94 ContentsManager.pre_save_hook
95 FileContentsManager.post_save_hook
96
97 A post-save hook has been registered that calls:
98
99 ipython nbconvert --to python [notebook]
100
101 which behaves similar to `--script`.
73 102 """)
74 103
104 self.post_save_hook = _post_save_script
105
75 106 post_save_hook = Any(None, config=True,
76 107 help="""Python callable or importstring thereof
77 108
78 109 to be called on the path of a file just saved.
79 110
80 111 This can be used to
81 112 This can be used to process the file on disk,
82 113 such as converting the notebook to other formats, such as Python or HTML via nbconvert
83 114
84 115 It will be called as (all arguments passed by keyword):
85 116
86 117 hook(os_path=os_path, model=model, contents_manager=instance)
87 118
88 119 path: the filesystem path to the file just written
89 120 model: the model representing the file
90 121 contents_manager: this ContentsManager instance
91 122 """
92 123 )
93 124 def _post_save_hook_changed(self, name, old, new):
94 125 if new and isinstance(new, string_types):
95 126 self.post_save_hook = import_item(self.post_save_hook)
96 127 elif new:
97 128 if not callable(new):
98 129 raise TraitError("post_save_hook must be callable")
99 130
100 131 def run_post_save_hook(self, model, os_path):
101 132 """Run the post-save hook if defined, and log errors"""
102 133 if self.post_save_hook:
103 134 try:
104 135 self.log.debug("Running post-save hook on %s", os_path)
105 136 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
106 137 except Exception:
107 138 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
108 139
109 140 def _root_dir_changed(self, name, old, new):
110 141 """Do a bit of validation of the root_dir."""
111 142 if not os.path.isabs(new):
112 143 # If we receive a non-absolute path, make it absolute.
113 144 self.root_dir = os.path.abspath(new)
114 145 return
115 146 if not os.path.isdir(new):
116 147 raise TraitError("%r is not a directory" % new)
117 148
118 149 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
119 150 help="""The directory name in which to keep file checkpoints
120 151
121 152 This is a path relative to the file's own directory.
122 153
123 154 By default, it is .ipynb_checkpoints
124 155 """
125 156 )
126 157
127 158 def _copy(self, src, dest):
128 159 """copy src to dest
129 160
130 161 like shutil.copy2, but log errors in copystat
131 162 """
132 163 shutil.copyfile(src, dest)
133 164 try:
134 165 shutil.copystat(src, dest)
135 166 except OSError as e:
136 167 self.log.debug("copystat on %s failed", dest, exc_info=True)
137 168
138 169 def _get_os_path(self, path):
139 170 """Given an API path, return its file system path.
140 171
141 172 Parameters
142 173 ----------
143 174 path : string
144 175 The relative API path to the named file.
145 176
146 177 Returns
147 178 -------
148 179 path : string
149 180 Native, absolute OS path to for a file.
150 181 """
151 182 return to_os_path(path, self.root_dir)
152 183
153 184 def dir_exists(self, path):
154 185 """Does the API-style path refer to an extant directory?
155 186
156 187 API-style wrapper for os.path.isdir
157 188
158 189 Parameters
159 190 ----------
160 191 path : string
161 192 The path to check. This is an API path (`/` separated,
162 193 relative to root_dir).
163 194
164 195 Returns
165 196 -------
166 197 exists : bool
167 198 Whether the path is indeed a directory.
168 199 """
169 200 path = path.strip('/')
170 201 os_path = self._get_os_path(path=path)
171 202 return os.path.isdir(os_path)
172 203
173 204 def is_hidden(self, path):
174 205 """Does the API style path correspond to a hidden directory or file?
175 206
176 207 Parameters
177 208 ----------
178 209 path : string
179 210 The path to check. This is an API path (`/` separated,
180 211 relative to root_dir).
181 212
182 213 Returns
183 214 -------
184 215 hidden : bool
185 216 Whether the path exists and is hidden.
186 217 """
187 218 path = path.strip('/')
188 219 os_path = self._get_os_path(path=path)
189 220 return is_hidden(os_path, self.root_dir)
190 221
191 222 def file_exists(self, path):
192 223 """Returns True if the file exists, else returns False.
193 224
194 225 API-style wrapper for os.path.isfile
195 226
196 227 Parameters
197 228 ----------
198 229 path : string
199 230 The relative path to the file (with '/' as separator)
200 231
201 232 Returns
202 233 -------
203 234 exists : bool
204 235 Whether the file exists.
205 236 """
206 237 path = path.strip('/')
207 238 os_path = self._get_os_path(path)
208 239 return os.path.isfile(os_path)
209 240
210 241 def exists(self, path):
211 242 """Returns True if the path exists, else returns False.
212 243
213 244 API-style wrapper for os.path.exists
214 245
215 246 Parameters
216 247 ----------
217 248 path : string
218 249 The API path to the file (with '/' as separator)
219 250
220 251 Returns
221 252 -------
222 253 exists : bool
223 254 Whether the target exists.
224 255 """
225 256 path = path.strip('/')
226 257 os_path = self._get_os_path(path=path)
227 258 return os.path.exists(os_path)
228 259
229 260 def _base_model(self, path):
230 261 """Build the common base of a contents model"""
231 262 os_path = self._get_os_path(path)
232 263 info = os.stat(os_path)
233 264 last_modified = tz.utcfromtimestamp(info.st_mtime)
234 265 created = tz.utcfromtimestamp(info.st_ctime)
235 266 # Create the base model.
236 267 model = {}
237 268 model['name'] = path.rsplit('/', 1)[-1]
238 269 model['path'] = path
239 270 model['last_modified'] = last_modified
240 271 model['created'] = created
241 272 model['content'] = None
242 273 model['format'] = None
243 274 model['mimetype'] = None
244 275 try:
245 276 model['writable'] = os.access(os_path, os.W_OK)
246 277 except OSError:
247 278 self.log.error("Failed to check write permissions on %s", os_path)
248 279 model['writable'] = False
249 280 return model
250 281
251 282 def _dir_model(self, path, content=True):
252 283 """Build a model for a directory
253 284
254 285 if content is requested, will include a listing of the directory
255 286 """
256 287 os_path = self._get_os_path(path)
257 288
258 289 four_o_four = u'directory does not exist: %r' % path
259 290
260 291 if not os.path.isdir(os_path):
261 292 raise web.HTTPError(404, four_o_four)
262 293 elif is_hidden(os_path, self.root_dir):
263 294 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
264 295 os_path
265 296 )
266 297 raise web.HTTPError(404, four_o_four)
267 298
268 299 model = self._base_model(path)
269 300 model['type'] = 'directory'
270 301 if content:
271 302 model['content'] = contents = []
272 303 os_dir = self._get_os_path(path)
273 304 for name in os.listdir(os_dir):
274 305 os_path = os.path.join(os_dir, name)
275 306 # skip over broken symlinks in listing
276 307 if not os.path.exists(os_path):
277 308 self.log.warn("%s doesn't exist", os_path)
278 309 continue
279 310 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
280 311 self.log.debug("%s not a regular file", os_path)
281 312 continue
282 313 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
283 314 contents.append(self.get(
284 315 path='%s/%s' % (path, name),
285 316 content=False)
286 317 )
287 318
288 319 model['format'] = 'json'
289 320
290 321 return model
291 322
292 323 def _file_model(self, path, content=True, format=None):
293 324 """Build a model for a file
294 325
295 326 if content is requested, include the file contents.
296 327
297 328 format:
298 329 If 'text', the contents will be decoded as UTF-8.
299 330 If 'base64', the raw bytes contents will be encoded as base64.
300 331 If not specified, try to decode as UTF-8, and fall back to base64
301 332 """
302 333 model = self._base_model(path)
303 334 model['type'] = 'file'
304 335
305 336 os_path = self._get_os_path(path)
306 337 model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
307 338
308 339 if content:
309 340 if not os.path.isfile(os_path):
310 341 # could be FIFO
311 342 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
312 343 with self.open(os_path, 'rb') as f:
313 344 bcontent = f.read()
314 345
315 346 if format != 'base64':
316 347 try:
317 348 model['content'] = bcontent.decode('utf8')
318 349 except UnicodeError as e:
319 350 if format == 'text':
320 351 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
321 352 else:
322 353 model['format'] = 'text'
323 354
324 355 if model['content'] is None:
325 356 model['content'] = base64.encodestring(bcontent).decode('ascii')
326 357 model['format'] = 'base64'
327 358
328 359 return model
329 360
330 361
331 362 def _notebook_model(self, path, content=True):
332 363 """Build a notebook model
333 364
334 365 if content is requested, the notebook content will be populated
335 366 as a JSON structure (not double-serialized)
336 367 """
337 368 model = self._base_model(path)
338 369 model['type'] = 'notebook'
339 370 if content:
340 371 os_path = self._get_os_path(path)
341 372 with self.open(os_path, 'r', encoding='utf-8') as f:
342 373 try:
343 374 nb = nbformat.read(f, as_version=4)
344 375 except Exception as e:
345 376 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
346 377 self.mark_trusted_cells(nb, path)
347 378 model['content'] = nb
348 379 model['format'] = 'json'
349 380 self.validate_notebook_model(model)
350 381 return model
351 382
352 383 def get(self, path, content=True, type_=None, format=None):
353 384 """ Takes a path for an entity and returns its model
354 385
355 386 Parameters
356 387 ----------
357 388 path : str
358 389 the API path that describes the relative path for the target
359 390 content : bool
360 391 Whether to include the contents in the reply
361 392 type_ : str, optional
362 393 The requested type - 'file', 'notebook', or 'directory'.
363 394 Will raise HTTPError 400 if the content doesn't match.
364 395 format : str, optional
365 396 The requested format for file contents. 'text' or 'base64'.
366 397 Ignored if this returns a notebook or directory model.
367 398
368 399 Returns
369 400 -------
370 401 model : dict
371 402 the contents model. If content=True, returns the contents
372 403 of the file or directory as well.
373 404 """
374 405 path = path.strip('/')
375 406
376 407 if not self.exists(path):
377 408 raise web.HTTPError(404, u'No such file or directory: %s' % path)
378 409
379 410 os_path = self._get_os_path(path)
380 411 if os.path.isdir(os_path):
381 412 if type_ not in (None, 'directory'):
382 413 raise web.HTTPError(400,
383 414 u'%s is a directory, not a %s' % (path, type_))
384 415 model = self._dir_model(path, content=content)
385 416 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
386 417 model = self._notebook_model(path, content=content)
387 418 else:
388 419 if type_ == 'directory':
389 420 raise web.HTTPError(400,
390 421 u'%s is not a directory')
391 422 model = self._file_model(path, content=content, format=format)
392 423 return model
393 424
394 425 def _save_notebook(self, os_path, model, path=''):
395 426 """save a notebook file"""
396 427 # Save the notebook file
397 428 nb = nbformat.from_dict(model['content'])
398 429
399 430 self.check_and_sign(nb, path)
400 431
401 432 with self.atomic_writing(os_path, encoding='utf-8') as f:
402 433 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
403 434
404 435 def _save_file(self, os_path, model, path=''):
405 436 """save a non-notebook file"""
406 437 fmt = model.get('format', None)
407 438 if fmt not in {'text', 'base64'}:
408 439 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
409 440 try:
410 441 content = model['content']
411 442 if fmt == 'text':
412 443 bcontent = content.encode('utf8')
413 444 else:
414 445 b64_bytes = content.encode('ascii')
415 446 bcontent = base64.decodestring(b64_bytes)
416 447 except Exception as e:
417 448 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
418 449 with self.atomic_writing(os_path, text=False) as f:
419 450 f.write(bcontent)
420 451
421 452 def _save_directory(self, os_path, model, path=''):
422 453 """create a directory"""
423 454 if is_hidden(os_path, self.root_dir):
424 455 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
425 456 if not os.path.exists(os_path):
426 457 with self.perm_to_403():
427 458 os.mkdir(os_path)
428 459 elif not os.path.isdir(os_path):
429 460 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
430 461 else:
431 462 self.log.debug("Directory %r already exists", os_path)
432 463
433 464 def save(self, model, path=''):
434 465 """Save the file model and return the model with no content."""
435 466 path = path.strip('/')
436 467
437 468 if 'type' not in model:
438 469 raise web.HTTPError(400, u'No file type provided')
439 470 if 'content' not in model and model['type'] != 'directory':
440 471 raise web.HTTPError(400, u'No file content provided')
441 472
442 473 self.run_pre_save_hook(model=model, path=path)
443 474
444 475 # One checkpoint should always exist
445 476 if self.file_exists(path) and not self.list_checkpoints(path):
446 477 self.create_checkpoint(path)
447 478
448 479 os_path = self._get_os_path(path)
449 480 self.log.debug("Saving %s", os_path)
450 481 try:
451 482 if model['type'] == 'notebook':
452 483 self._save_notebook(os_path, model, path)
453 484 elif model['type'] == 'file':
454 485 self._save_file(os_path, model, path)
455 486 elif model['type'] == 'directory':
456 487 self._save_directory(os_path, model, path)
457 488 else:
458 489 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
459 490 except web.HTTPError:
460 491 raise
461 492 except Exception as e:
462 493 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
463 494 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
464 495
465 496 validation_message = None
466 497 if model['type'] == 'notebook':
467 498 self.validate_notebook_model(model)
468 499 validation_message = model.get('message', None)
469 500
470 501 model = self.get(path, content=False)
471 502 if validation_message:
472 503 model['message'] = validation_message
473 504
474 505 self.run_post_save_hook(model=model, os_path=os_path)
475 506
476 507 return model
477 508
478 509 def update(self, model, path):
479 510 """Update the file's path
480 511
481 512 For use in PATCH requests, to enable renaming a file without
482 513 re-uploading its contents. Only used for renaming at the moment.
483 514 """
484 515 path = path.strip('/')
485 516 new_path = model.get('path', path).strip('/')
486 517 if path != new_path:
487 518 self.rename(path, new_path)
488 519 model = self.get(new_path, content=False)
489 520 return model
490 521
491 522 def delete(self, path):
492 523 """Delete file at path."""
493 524 path = path.strip('/')
494 525 os_path = self._get_os_path(path)
495 526 rm = os.unlink
496 527 if os.path.isdir(os_path):
497 528 listing = os.listdir(os_path)
498 529 # don't delete non-empty directories (checkpoints dir doesn't count)
499 530 if listing and listing != [self.checkpoint_dir]:
500 531 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
501 532 elif not os.path.isfile(os_path):
502 533 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
503 534
504 535 # clear checkpoints
505 536 for checkpoint in self.list_checkpoints(path):
506 537 checkpoint_id = checkpoint['id']
507 538 cp_path = self.get_checkpoint_path(checkpoint_id, path)
508 539 if os.path.isfile(cp_path):
509 540 self.log.debug("Unlinking checkpoint %s", cp_path)
510 541 with self.perm_to_403():
511 542 rm(cp_path)
512 543
513 544 if os.path.isdir(os_path):
514 545 self.log.debug("Removing directory %s", os_path)
515 546 with self.perm_to_403():
516 547 shutil.rmtree(os_path)
517 548 else:
518 549 self.log.debug("Unlinking file %s", os_path)
519 550 with self.perm_to_403():
520 551 rm(os_path)
521 552
522 553 def rename(self, old_path, new_path):
523 554 """Rename a file."""
524 555 old_path = old_path.strip('/')
525 556 new_path = new_path.strip('/')
526 557 if new_path == old_path:
527 558 return
528 559
529 560 new_os_path = self._get_os_path(new_path)
530 561 old_os_path = self._get_os_path(old_path)
531 562
532 563 # Should we proceed with the move?
533 564 if os.path.exists(new_os_path):
534 565 raise web.HTTPError(409, u'File already exists: %s' % new_path)
535 566
536 567 # Move the file
537 568 try:
538 569 with self.perm_to_403():
539 570 shutil.move(old_os_path, new_os_path)
540 571 except web.HTTPError:
541 572 raise
542 573 except Exception as e:
543 574 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
544 575
545 576 # Move the checkpoints
546 577 old_checkpoints = self.list_checkpoints(old_path)
547 578 for cp in old_checkpoints:
548 579 checkpoint_id = cp['id']
549 580 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
550 581 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
551 582 if os.path.isfile(old_cp_path):
552 583 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
553 584 with self.perm_to_403():
554 585 shutil.move(old_cp_path, new_cp_path)
555 586
556 587 # Checkpoint-related utilities
557 588
558 589 def get_checkpoint_path(self, checkpoint_id, path):
559 590 """find the path to a checkpoint"""
560 591 path = path.strip('/')
561 592 parent, name = ('/' + path).rsplit('/', 1)
562 593 parent = parent.strip('/')
563 594 basename, ext = os.path.splitext(name)
564 595 filename = u"{name}-{checkpoint_id}{ext}".format(
565 596 name=basename,
566 597 checkpoint_id=checkpoint_id,
567 598 ext=ext,
568 599 )
569 600 os_path = self._get_os_path(path=parent)
570 601 cp_dir = os.path.join(os_path, self.checkpoint_dir)
571 602 with self.perm_to_403():
572 603 ensure_dir_exists(cp_dir)
573 604 cp_path = os.path.join(cp_dir, filename)
574 605 return cp_path
575 606
576 607 def get_checkpoint_model(self, checkpoint_id, path):
577 608 """construct the info dict for a given checkpoint"""
578 609 path = path.strip('/')
579 610 cp_path = self.get_checkpoint_path(checkpoint_id, path)
580 611 stats = os.stat(cp_path)
581 612 last_modified = tz.utcfromtimestamp(stats.st_mtime)
582 613 info = dict(
583 614 id = checkpoint_id,
584 615 last_modified = last_modified,
585 616 )
586 617 return info
587 618
588 619 # public checkpoint API
589 620
590 621 def create_checkpoint(self, path):
591 622 """Create a checkpoint from the current state of a file"""
592 623 path = path.strip('/')
593 624 if not self.file_exists(path):
594 625 raise web.HTTPError(404)
595 626 src_path = self._get_os_path(path)
596 627 # only the one checkpoint ID:
597 628 checkpoint_id = u"checkpoint"
598 629 cp_path = self.get_checkpoint_path(checkpoint_id, path)
599 630 self.log.debug("creating checkpoint for %s", path)
600 631 with self.perm_to_403():
601 632 self._copy(src_path, cp_path)
602 633
603 634 # return the checkpoint info
604 635 return self.get_checkpoint_model(checkpoint_id, path)
605 636
606 637 def list_checkpoints(self, path):
607 638 """list the checkpoints for a given file
608 639
609 640 This contents manager currently only supports one checkpoint per file.
610 641 """
611 642 path = path.strip('/')
612 643 checkpoint_id = "checkpoint"
613 644 os_path = self.get_checkpoint_path(checkpoint_id, path)
614 645 if not os.path.exists(os_path):
615 646 return []
616 647 else:
617 648 return [self.get_checkpoint_model(checkpoint_id, path)]
618 649
619 650
620 651 def restore_checkpoint(self, checkpoint_id, path):
621 652 """restore a file to a checkpointed state"""
622 653 path = path.strip('/')
623 654 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
624 655 nb_path = self._get_os_path(path)
625 656 cp_path = self.get_checkpoint_path(checkpoint_id, path)
626 657 if not os.path.isfile(cp_path):
627 658 self.log.debug("checkpoint file does not exist: %s", cp_path)
628 659 raise web.HTTPError(404,
629 660 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
630 661 )
631 662 # ensure notebook is readable (never restore from an unreadable notebook)
632 663 if cp_path.endswith('.ipynb'):
633 664 with self.open(cp_path, 'r', encoding='utf-8') as f:
634 665 nbformat.read(f, as_version=4)
635 666 self.log.debug("copying %s -> %s", cp_path, nb_path)
636 667 with self.perm_to_403():
637 668 self._copy(cp_path, nb_path)
638 669
639 670 def delete_checkpoint(self, checkpoint_id, path):
640 671 """delete a file's checkpoint"""
641 672 path = path.strip('/')
642 673 cp_path = self.get_checkpoint_path(checkpoint_id, path)
643 674 if not os.path.isfile(cp_path):
644 675 raise web.HTTPError(404,
645 676 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
646 677 )
647 678 self.log.debug("unlinking %s", cp_path)
648 679 os.unlink(cp_path)
649 680
650 681 def info_string(self):
651 682 return "Serving notebooks from local directory: %s" % self.root_dir
652 683
653 684 def get_kernel_path(self, path, model=None):
654 685 """Return the initial API path of a kernel associated with a given notebook"""
655 686 if '/' in path:
656 687 parent_dir = path.rsplit('/', 1)[0]
657 688 else:
658 689 parent_dir = ''
659 690 return parent_dir
General Comments 0
You need to be logged in to leave comments. Login now