##// END OF EJS Templates
MAINT: Return dicts from CheckpointManager.get_checkpoint....
Scott Sanderson -
Show More
@@ -1,761 +1,772 b''
1 """A contents manager that uses the local file system for storage."""
1 """A contents manager that uses the local file system for storage."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 from contextlib import contextmanager
7 from contextlib import contextmanager
8 import errno
8 import errno
9 import io
9 import io
10 import os
10 import os
11 import shutil
11 import shutil
12 import mimetypes
12 import mimetypes
13
13
14 from tornado import web
14 from tornado import web
15
15
16 from .manager import (
16 from .manager import (
17 CheckpointManager,
17 CheckpointManager,
18 ContentsManager,
18 ContentsManager,
19 )
19 )
20 from IPython import nbformat
20 from IPython import nbformat
21 from IPython.utils.io import atomic_writing
21 from IPython.utils.io import atomic_writing
22 from IPython.utils.importstring import import_item
22 from IPython.utils.importstring import import_item
23 from IPython.utils.path import ensure_dir_exists
23 from IPython.utils.path import ensure_dir_exists
24 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
24 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
25 from IPython.utils.py3compat import getcwd, string_types, str_to_unicode
25 from IPython.utils.py3compat import getcwd, string_types, str_to_unicode
26 from IPython.utils import tz
26 from IPython.utils import tz
27 from IPython.html.utils import (
27 from IPython.html.utils import (
28 is_hidden,
28 is_hidden,
29 to_api_path,
29 to_api_path,
30 to_os_path,
30 to_os_path,
31 )
31 )
32
32
33 _script_exporter = None
33 _script_exporter = None
34
34
35 def _post_save_script(model, os_path, contents_manager, **kwargs):
35 def _post_save_script(model, os_path, contents_manager, **kwargs):
36 """convert notebooks to Python script after save with nbconvert
36 """convert notebooks to Python script after save with nbconvert
37
37
38 replaces `ipython notebook --script`
38 replaces `ipython notebook --script`
39 """
39 """
40 from IPython.nbconvert.exporters.script import ScriptExporter
40 from IPython.nbconvert.exporters.script import ScriptExporter
41
41
42 if model['type'] != 'notebook':
42 if model['type'] != 'notebook':
43 return
43 return
44
44
45 global _script_exporter
45 global _script_exporter
46 if _script_exporter is None:
46 if _script_exporter is None:
47 _script_exporter = ScriptExporter(parent=contents_manager)
47 _script_exporter = ScriptExporter(parent=contents_manager)
48 log = contents_manager.log
48 log = contents_manager.log
49
49
50 base, ext = os.path.splitext(os_path)
50 base, ext = os.path.splitext(os_path)
51 py_fname = base + '.py'
51 py_fname = base + '.py'
52 script, resources = _script_exporter.from_filename(os_path)
52 script, resources = _script_exporter.from_filename(os_path)
53 script_fname = base + resources.get('output_extension', '.txt')
53 script_fname = base + resources.get('output_extension', '.txt')
54 log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
54 log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
55 with io.open(script_fname, 'w', encoding='utf-8') as f:
55 with io.open(script_fname, 'w', encoding='utf-8') as f:
56 f.write(script)
56 f.write(script)
57
57
58
58
59 class FileManagerMixin(object):
59 class FileManagerMixin(object):
60 """
60 """
61 Mixin for ContentsAPI classes that interact with the filesystem.
61 Mixin for ContentsAPI classes that interact with the filesystem.
62
62
63 Provides facilities for reading, writing, and copying both notebooks and
63 Provides facilities for reading, writing, and copying both notebooks and
64 generic files.
64 generic files.
65
65
66 Shared by FileContentsManager and FileCheckpointManager.
66 Shared by FileContentsManager and FileCheckpointManager.
67
67
68 Note
68 Note
69 ----
69 ----
70 Classes using this mixin must provide the following attributes:
70 Classes using this mixin must provide the following attributes:
71
71
72 root_dir : unicode
72 root_dir : unicode
73 A directory against against which API-style paths are to be resolved.
73 A directory against against which API-style paths are to be resolved.
74
74
75 log : logging.Logger
75 log : logging.Logger
76 """
76 """
77
77
78 @contextmanager
78 @contextmanager
79 def open(self, os_path, *args, **kwargs):
79 def open(self, os_path, *args, **kwargs):
80 """wrapper around io.open that turns permission errors into 403"""
80 """wrapper around io.open that turns permission errors into 403"""
81 with self.perm_to_403(os_path):
81 with self.perm_to_403(os_path):
82 with io.open(os_path, *args, **kwargs) as f:
82 with io.open(os_path, *args, **kwargs) as f:
83 yield f
83 yield f
84
84
85 @contextmanager
85 @contextmanager
86 def atomic_writing(self, os_path, *args, **kwargs):
86 def atomic_writing(self, os_path, *args, **kwargs):
87 """wrapper around atomic_writing that turns permission errors into 403"""
87 """wrapper around atomic_writing that turns permission errors into 403"""
88 with self.perm_to_403(os_path):
88 with self.perm_to_403(os_path):
89 with atomic_writing(os_path, *args, **kwargs) as f:
89 with atomic_writing(os_path, *args, **kwargs) as f:
90 yield f
90 yield f
91
91
92 @contextmanager
92 @contextmanager
93 def perm_to_403(self, os_path=''):
93 def perm_to_403(self, os_path=''):
94 """context manager for turning permission errors into 403."""
94 """context manager for turning permission errors into 403."""
95 try:
95 try:
96 yield
96 yield
97 except OSError as e:
97 except OSError as e:
98 if e.errno in {errno.EPERM, errno.EACCES}:
98 if e.errno in {errno.EPERM, errno.EACCES}:
99 # make 403 error message without root prefix
99 # make 403 error message without root prefix
100 # this may not work perfectly on unicode paths on Python 2,
100 # this may not work perfectly on unicode paths on Python 2,
101 # but nobody should be doing that anyway.
101 # but nobody should be doing that anyway.
102 if not os_path:
102 if not os_path:
103 os_path = str_to_unicode(e.filename or 'unknown file')
103 os_path = str_to_unicode(e.filename or 'unknown file')
104 path = to_api_path(os_path, root=self.root_dir)
104 path = to_api_path(os_path, root=self.root_dir)
105 raise web.HTTPError(403, u'Permission denied: %s' % path)
105 raise web.HTTPError(403, u'Permission denied: %s' % path)
106 else:
106 else:
107 raise
107 raise
108
108
109 def _copy(self, src, dest):
109 def _copy(self, src, dest):
110 """copy src to dest
110 """copy src to dest
111
111
112 like shutil.copy2, but log errors in copystat
112 like shutil.copy2, but log errors in copystat
113 """
113 """
114 shutil.copyfile(src, dest)
114 shutil.copyfile(src, dest)
115 try:
115 try:
116 shutil.copystat(src, dest)
116 shutil.copystat(src, dest)
117 except OSError:
117 except OSError:
118 self.log.debug("copystat on %s failed", dest, exc_info=True)
118 self.log.debug("copystat on %s failed", dest, exc_info=True)
119
119
120 def _get_os_path(self, path):
120 def _get_os_path(self, path):
121 """Given an API path, return its file system path.
121 """Given an API path, return its file system path.
122
122
123 Parameters
123 Parameters
124 ----------
124 ----------
125 path : string
125 path : string
126 The relative API path to the named file.
126 The relative API path to the named file.
127
127
128 Returns
128 Returns
129 -------
129 -------
130 path : string
130 path : string
131 Native, absolute OS path to for a file.
131 Native, absolute OS path to for a file.
132 """
132 """
133 return to_os_path(path, self.root_dir)
133 return to_os_path(path, self.root_dir)
134
134
135 def _read_notebook(self, os_path, as_version=4):
135 def _read_notebook(self, os_path, as_version=4):
136 """Read a notebook from an os path."""
136 """Read a notebook from an os path."""
137 with self.open(os_path, 'r', encoding='utf-8') as f:
137 with self.open(os_path, 'r', encoding='utf-8') as f:
138 try:
138 try:
139 return nbformat.read(f, as_version=as_version)
139 return nbformat.read(f, as_version=as_version)
140 except Exception as e:
140 except Exception as e:
141 raise web.HTTPError(
141 raise web.HTTPError(
142 400,
142 400,
143 u"Unreadable Notebook: %s %r" % (os_path, e),
143 u"Unreadable Notebook: %s %r" % (os_path, e),
144 )
144 )
145
145
146 def _save_notebook(self, os_path, nb):
146 def _save_notebook(self, os_path, nb):
147 """Save a notebook to an os_path."""
147 """Save a notebook to an os_path."""
148 with self.atomic_writing(os_path, encoding='utf-8') as f:
148 with self.atomic_writing(os_path, encoding='utf-8') as f:
149 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
149 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
150
150
151 def _read_file(self, os_path, format):
151 def _read_file(self, os_path, format):
152 """Read a non-notebook file.
152 """Read a non-notebook file.
153
153
154 os_path: The path to be read.
154 os_path: The path to be read.
155 format:
155 format:
156 If 'text', the contents will be decoded as UTF-8.
156 If 'text', the contents will be decoded as UTF-8.
157 If 'base64', the raw bytes contents will be encoded as base64.
157 If 'base64', the raw bytes contents will be encoded as base64.
158 If not specified, try to decode as UTF-8, and fall back to base64
158 If not specified, try to decode as UTF-8, and fall back to base64
159 """
159 """
160 if not os.path.isfile(os_path):
160 if not os.path.isfile(os_path):
161 raise web.HTTPError(400, "Cannot read non-file %s" % os_path)
161 raise web.HTTPError(400, "Cannot read non-file %s" % os_path)
162
162
163 with self.open(os_path, 'rb') as f:
163 with self.open(os_path, 'rb') as f:
164 bcontent = f.read()
164 bcontent = f.read()
165
165
166 if format is None or format == 'text':
166 if format is None or format == 'text':
167 # Try to interpret as unicode if format is unknown or if unicode
167 # Try to interpret as unicode if format is unknown or if unicode
168 # was explicitly requested.
168 # was explicitly requested.
169 try:
169 try:
170 return bcontent.decode('utf8'), 'text'
170 return bcontent.decode('utf8'), 'text'
171 except UnicodeError as e:
171 except UnicodeError as e:
172 if format == 'text':
172 if format == 'text':
173 raise web.HTTPError(
173 raise web.HTTPError(
174 400,
174 400,
175 "%s is not UTF-8 encoded" % os_path,
175 "%s is not UTF-8 encoded" % os_path,
176 reason='bad format',
176 reason='bad format',
177 )
177 )
178 return base64.encodestring(bcontent).decode('ascii'), 'base64'
178 return base64.encodestring(bcontent).decode('ascii'), 'base64'
179
179
180 def _save_file(self, os_path, content, format):
180 def _save_file(self, os_path, content, format):
181 """Save content of a generic file."""
181 """Save content of a generic file."""
182 if format not in {'text', 'base64'}:
182 if format not in {'text', 'base64'}:
183 raise web.HTTPError(
183 raise web.HTTPError(
184 400,
184 400,
185 "Must specify format of file contents as 'text' or 'base64'",
185 "Must specify format of file contents as 'text' or 'base64'",
186 )
186 )
187 try:
187 try:
188 if format == 'text':
188 if format == 'text':
189 bcontent = content.encode('utf8')
189 bcontent = content.encode('utf8')
190 else:
190 else:
191 b64_bytes = content.encode('ascii')
191 b64_bytes = content.encode('ascii')
192 bcontent = base64.decodestring(b64_bytes)
192 bcontent = base64.decodestring(b64_bytes)
193 except Exception as e:
193 except Exception as e:
194 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
194 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
195
195
196 with self.atomic_writing(os_path, text=False) as f:
196 with self.atomic_writing(os_path, text=False) as f:
197 f.write(bcontent)
197 f.write(bcontent)
198
198
199
199
200 class FileCheckpointManager(FileManagerMixin, CheckpointManager):
200 class FileCheckpointManager(FileManagerMixin, CheckpointManager):
201 """
201 """
202 A CheckpointManager that caches checkpoints for files in adjacent
202 A CheckpointManager that caches checkpoints for files in adjacent
203 directories.
203 directories.
204 """
204 """
205
205
206 checkpoint_dir = Unicode(
206 checkpoint_dir = Unicode(
207 '.ipynb_checkpoints',
207 '.ipynb_checkpoints',
208 config=True,
208 config=True,
209 help="""The directory name in which to keep file checkpoints
209 help="""The directory name in which to keep file checkpoints
210
210
211 This is a path relative to the file's own directory.
211 This is a path relative to the file's own directory.
212
212
213 By default, it is .ipynb_checkpoints
213 By default, it is .ipynb_checkpoints
214 """,
214 """,
215 )
215 )
216
216
217 root_dir = Unicode(config=True)
217 root_dir = Unicode(config=True)
218
218
219 def _root_dir_default(self):
219 def _root_dir_default(self):
220 try:
220 try:
221 return self.parent.root_dir
221 return self.parent.root_dir
222 except AttributeError:
222 except AttributeError:
223 return getcwd()
223 return getcwd()
224
224
225 # public checkpoint API
225 # public checkpoint API
226 def create_file_checkpoint(self, content, format, path):
226 def create_file_checkpoint(self, content, format, path):
227 """Create a checkpoint from the current content of a notebook."""
227 """Create a checkpoint from the current content of a notebook."""
228 path = path.strip('/')
228 path = path.strip('/')
229 # only the one checkpoint ID:
229 # only the one checkpoint ID:
230 checkpoint_id = u"checkpoint"
230 checkpoint_id = u"checkpoint"
231 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
231 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
232 self.log.debug("creating checkpoint for %s", path)
232 self.log.debug("creating checkpoint for %s", path)
233 with self.perm_to_403():
233 with self.perm_to_403():
234 self._save_file(os_checkpoint_path, content, format=format)
234 self._save_file(os_checkpoint_path, content, format=format)
235
235
236 # return the checkpoint info
236 # return the checkpoint info
237 return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
237 return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
238
238
239 def create_notebook_checkpoint(self, nb, path):
239 def create_notebook_checkpoint(self, nb, path):
240 """Create a checkpoint from the current content of a notebook."""
240 """Create a checkpoint from the current content of a notebook."""
241 path = path.strip('/')
241 path = path.strip('/')
242 # only the one checkpoint ID:
242 # only the one checkpoint ID:
243 checkpoint_id = u"checkpoint"
243 checkpoint_id = u"checkpoint"
244 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
244 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
245 self.log.debug("creating checkpoint for %s", path)
245 self.log.debug("creating checkpoint for %s", path)
246 with self.perm_to_403():
246 with self.perm_to_403():
247 self._save_notebook(os_checkpoint_path, nb)
247 self._save_notebook(os_checkpoint_path, nb)
248
248
249 # return the checkpoint info
249 # return the checkpoint info
250 return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
250 return self.checkpoint_model(checkpoint_id, os_checkpoint_path)
251
251
252 def get_checkpoint(self, checkpoint_id, path, type):
252 def get_checkpoint(self, checkpoint_id, path, type):
253 """Get the content of a checkpoint.
253 """Get the content of a checkpoint.
254
254
255 Returns a pair of (content, type).
255 Returns a model suitable for passing to ContentsManager.save.
256 """
256 """
257 path = path.strip('/')
257 path = path.strip('/')
258 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
258 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
259 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
259 os_checkpoint_path = self.checkpoint_path(checkpoint_id, path)
260 if not os.path.isfile(os_checkpoint_path):
260 if not os.path.isfile(os_checkpoint_path):
261 self.no_such_checkpoint(path, checkpoint_id)
261 self.no_such_checkpoint(path, checkpoint_id)
262 if type == 'notebook':
262 if type == 'notebook':
263 return self._read_notebook(os_checkpoint_path, as_version=4), None
263 return {
264 'type': type,
265 'content': self._read_notebook(
266 os_checkpoint_path,
267 as_version=4,
268 ),
269 }
264 else:
270 else:
265 return self._read_file(os_checkpoint_path, format=None)
271 content, format = self._read_file(os_checkpoint_path, format=None)
272 return {
273 'type': type,
274 'content': content,
275 'format': format,
276 }
266
277
267 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
278 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
268 """Rename a checkpoint from old_path to new_path."""
279 """Rename a checkpoint from old_path to new_path."""
269 old_cp_path = self.checkpoint_path(checkpoint_id, old_path)
280 old_cp_path = self.checkpoint_path(checkpoint_id, old_path)
270 new_cp_path = self.checkpoint_path(checkpoint_id, new_path)
281 new_cp_path = self.checkpoint_path(checkpoint_id, new_path)
271 if os.path.isfile(old_cp_path):
282 if os.path.isfile(old_cp_path):
272 self.log.debug(
283 self.log.debug(
273 "Renaming checkpoint %s -> %s",
284 "Renaming checkpoint %s -> %s",
274 old_cp_path,
285 old_cp_path,
275 new_cp_path,
286 new_cp_path,
276 )
287 )
277 with self.perm_to_403():
288 with self.perm_to_403():
278 shutil.move(old_cp_path, new_cp_path)
289 shutil.move(old_cp_path, new_cp_path)
279
290
280 def delete_checkpoint(self, checkpoint_id, path):
291 def delete_checkpoint(self, checkpoint_id, path):
281 """delete a file's checkpoint"""
292 """delete a file's checkpoint"""
282 path = path.strip('/')
293 path = path.strip('/')
283 cp_path = self.checkpoint_path(checkpoint_id, path)
294 cp_path = self.checkpoint_path(checkpoint_id, path)
284 if not os.path.isfile(cp_path):
295 if not os.path.isfile(cp_path):
285 self.no_such_checkpoint(path, checkpoint_id)
296 self.no_such_checkpoint(path, checkpoint_id)
286
297
287 self.log.debug("unlinking %s", cp_path)
298 self.log.debug("unlinking %s", cp_path)
288 with self.perm_to_403():
299 with self.perm_to_403():
289 os.unlink(cp_path)
300 os.unlink(cp_path)
290
301
291 def list_checkpoints(self, path):
302 def list_checkpoints(self, path):
292 """list the checkpoints for a given file
303 """list the checkpoints for a given file
293
304
294 This contents manager currently only supports one checkpoint per file.
305 This contents manager currently only supports one checkpoint per file.
295 """
306 """
296 path = path.strip('/')
307 path = path.strip('/')
297 checkpoint_id = "checkpoint"
308 checkpoint_id = "checkpoint"
298 os_path = self.checkpoint_path(checkpoint_id, path)
309 os_path = self.checkpoint_path(checkpoint_id, path)
299 if not os.path.isfile(os_path):
310 if not os.path.isfile(os_path):
300 return []
311 return []
301 else:
312 else:
302 return [self.checkpoint_model(checkpoint_id, os_path)]
313 return [self.checkpoint_model(checkpoint_id, os_path)]
303
314
304 # Checkpoint-related utilities
315 # Checkpoint-related utilities
305 def checkpoint_path(self, checkpoint_id, path):
316 def checkpoint_path(self, checkpoint_id, path):
306 """find the path to a checkpoint"""
317 """find the path to a checkpoint"""
307 path = path.strip('/')
318 path = path.strip('/')
308 parent, name = ('/' + path).rsplit('/', 1)
319 parent, name = ('/' + path).rsplit('/', 1)
309 parent = parent.strip('/')
320 parent = parent.strip('/')
310 basename, ext = os.path.splitext(name)
321 basename, ext = os.path.splitext(name)
311 filename = u"{name}-{checkpoint_id}{ext}".format(
322 filename = u"{name}-{checkpoint_id}{ext}".format(
312 name=basename,
323 name=basename,
313 checkpoint_id=checkpoint_id,
324 checkpoint_id=checkpoint_id,
314 ext=ext,
325 ext=ext,
315 )
326 )
316 os_path = self._get_os_path(path=parent)
327 os_path = self._get_os_path(path=parent)
317 cp_dir = os.path.join(os_path, self.checkpoint_dir)
328 cp_dir = os.path.join(os_path, self.checkpoint_dir)
318 with self.perm_to_403():
329 with self.perm_to_403():
319 ensure_dir_exists(cp_dir)
330 ensure_dir_exists(cp_dir)
320 cp_path = os.path.join(cp_dir, filename)
331 cp_path = os.path.join(cp_dir, filename)
321 return cp_path
332 return cp_path
322
333
323 def checkpoint_model(self, checkpoint_id, os_path):
334 def checkpoint_model(self, checkpoint_id, os_path):
324 """construct the info dict for a given checkpoint"""
335 """construct the info dict for a given checkpoint"""
325 stats = os.stat(os_path)
336 stats = os.stat(os_path)
326 last_modified = tz.utcfromtimestamp(stats.st_mtime)
337 last_modified = tz.utcfromtimestamp(stats.st_mtime)
327 info = dict(
338 info = dict(
328 id=checkpoint_id,
339 id=checkpoint_id,
329 last_modified=last_modified,
340 last_modified=last_modified,
330 )
341 )
331 return info
342 return info
332
343
333 # Error Handling
344 # Error Handling
334 def no_such_checkpoint(self, path, checkpoint_id):
345 def no_such_checkpoint(self, path, checkpoint_id):
335 raise web.HTTPError(
346 raise web.HTTPError(
336 404,
347 404,
337 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
348 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
338 )
349 )
339
350
340
351
341 class FileContentsManager(FileManagerMixin, ContentsManager):
352 class FileContentsManager(FileManagerMixin, ContentsManager):
342
353
343 root_dir = Unicode(config=True)
354 root_dir = Unicode(config=True)
344
355
345 def _root_dir_default(self):
356 def _root_dir_default(self):
346 try:
357 try:
347 return self.parent.notebook_dir
358 return self.parent.notebook_dir
348 except AttributeError:
359 except AttributeError:
349 return getcwd()
360 return getcwd()
350
361
351 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
362 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
352 def _save_script_changed(self):
363 def _save_script_changed(self):
353 self.log.warn("""
364 self.log.warn("""
354 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
365 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
355
366
356 ContentsManager.pre_save_hook
367 ContentsManager.pre_save_hook
357 FileContentsManager.post_save_hook
368 FileContentsManager.post_save_hook
358
369
359 A post-save hook has been registered that calls:
370 A post-save hook has been registered that calls:
360
371
361 ipython nbconvert --to script [notebook]
372 ipython nbconvert --to script [notebook]
362
373
363 which behaves similarly to `--script`.
374 which behaves similarly to `--script`.
364 """)
375 """)
365
376
366 self.post_save_hook = _post_save_script
377 self.post_save_hook = _post_save_script
367
378
368 post_save_hook = Any(None, config=True,
379 post_save_hook = Any(None, config=True,
369 help="""Python callable or importstring thereof
380 help="""Python callable or importstring thereof
370
381
371 to be called on the path of a file just saved.
382 to be called on the path of a file just saved.
372
383
373 This can be used to process the file on disk,
384 This can be used to process the file on disk,
374 such as converting the notebook to a script or HTML via nbconvert.
385 such as converting the notebook to a script or HTML via nbconvert.
375
386
376 It will be called as (all arguments passed by keyword):
387 It will be called as (all arguments passed by keyword):
377
388
378 hook(os_path=os_path, model=model, contents_manager=instance)
389 hook(os_path=os_path, model=model, contents_manager=instance)
379
390
380 path: the filesystem path to the file just written
391 path: the filesystem path to the file just written
381 model: the model representing the file
392 model: the model representing the file
382 contents_manager: this ContentsManager instance
393 contents_manager: this ContentsManager instance
383 """
394 """
384 )
395 )
385 def _post_save_hook_changed(self, name, old, new):
396 def _post_save_hook_changed(self, name, old, new):
386 if new and isinstance(new, string_types):
397 if new and isinstance(new, string_types):
387 self.post_save_hook = import_item(self.post_save_hook)
398 self.post_save_hook = import_item(self.post_save_hook)
388 elif new:
399 elif new:
389 if not callable(new):
400 if not callable(new):
390 raise TraitError("post_save_hook must be callable")
401 raise TraitError("post_save_hook must be callable")
391
402
392 def run_post_save_hook(self, model, os_path):
403 def run_post_save_hook(self, model, os_path):
393 """Run the post-save hook if defined, and log errors"""
404 """Run the post-save hook if defined, and log errors"""
394 if self.post_save_hook:
405 if self.post_save_hook:
395 try:
406 try:
396 self.log.debug("Running post-save hook on %s", os_path)
407 self.log.debug("Running post-save hook on %s", os_path)
397 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
408 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
398 except Exception:
409 except Exception:
399 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
410 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
400
411
401 def _root_dir_changed(self, name, old, new):
412 def _root_dir_changed(self, name, old, new):
402 """Do a bit of validation of the root_dir."""
413 """Do a bit of validation of the root_dir."""
403 if not os.path.isabs(new):
414 if not os.path.isabs(new):
404 # If we receive a non-absolute path, make it absolute.
415 # If we receive a non-absolute path, make it absolute.
405 self.root_dir = os.path.abspath(new)
416 self.root_dir = os.path.abspath(new)
406 return
417 return
407 if not os.path.isdir(new):
418 if not os.path.isdir(new):
408 raise TraitError("%r is not a directory" % new)
419 raise TraitError("%r is not a directory" % new)
409
420
410 def _checkpoint_manager_class_default(self):
421 def _checkpoint_manager_class_default(self):
411 return FileCheckpointManager
422 return FileCheckpointManager
412
423
413 def is_hidden(self, path):
424 def is_hidden(self, path):
414 """Does the API style path correspond to a hidden directory or file?
425 """Does the API style path correspond to a hidden directory or file?
415
426
416 Parameters
427 Parameters
417 ----------
428 ----------
418 path : string
429 path : string
419 The path to check. This is an API path (`/` separated,
430 The path to check. This is an API path (`/` separated,
420 relative to root_dir).
431 relative to root_dir).
421
432
422 Returns
433 Returns
423 -------
434 -------
424 hidden : bool
435 hidden : bool
425 Whether the path exists and is hidden.
436 Whether the path exists and is hidden.
426 """
437 """
427 path = path.strip('/')
438 path = path.strip('/')
428 os_path = self._get_os_path(path=path)
439 os_path = self._get_os_path(path=path)
429 return is_hidden(os_path, self.root_dir)
440 return is_hidden(os_path, self.root_dir)
430
441
431 def file_exists(self, path):
442 def file_exists(self, path):
432 """Returns True if the file exists, else returns False.
443 """Returns True if the file exists, else returns False.
433
444
434 API-style wrapper for os.path.isfile
445 API-style wrapper for os.path.isfile
435
446
436 Parameters
447 Parameters
437 ----------
448 ----------
438 path : string
449 path : string
439 The relative path to the file (with '/' as separator)
450 The relative path to the file (with '/' as separator)
440
451
441 Returns
452 Returns
442 -------
453 -------
443 exists : bool
454 exists : bool
444 Whether the file exists.
455 Whether the file exists.
445 """
456 """
446 path = path.strip('/')
457 path = path.strip('/')
447 os_path = self._get_os_path(path)
458 os_path = self._get_os_path(path)
448 return os.path.isfile(os_path)
459 return os.path.isfile(os_path)
449
460
450 def dir_exists(self, path):
461 def dir_exists(self, path):
451 """Does the API-style path refer to an extant directory?
462 """Does the API-style path refer to an extant directory?
452
463
453 API-style wrapper for os.path.isdir
464 API-style wrapper for os.path.isdir
454
465
455 Parameters
466 Parameters
456 ----------
467 ----------
457 path : string
468 path : string
458 The path to check. This is an API path (`/` separated,
469 The path to check. This is an API path (`/` separated,
459 relative to root_dir).
470 relative to root_dir).
460
471
461 Returns
472 Returns
462 -------
473 -------
463 exists : bool
474 exists : bool
464 Whether the path is indeed a directory.
475 Whether the path is indeed a directory.
465 """
476 """
466 path = path.strip('/')
477 path = path.strip('/')
467 os_path = self._get_os_path(path=path)
478 os_path = self._get_os_path(path=path)
468 return os.path.isdir(os_path)
479 return os.path.isdir(os_path)
469
480
470 def exists(self, path):
481 def exists(self, path):
471 """Returns True if the path exists, else returns False.
482 """Returns True if the path exists, else returns False.
472
483
473 API-style wrapper for os.path.exists
484 API-style wrapper for os.path.exists
474
485
475 Parameters
486 Parameters
476 ----------
487 ----------
477 path : string
488 path : string
478 The API path to the file (with '/' as separator)
489 The API path to the file (with '/' as separator)
479
490
480 Returns
491 Returns
481 -------
492 -------
482 exists : bool
493 exists : bool
483 Whether the target exists.
494 Whether the target exists.
484 """
495 """
485 path = path.strip('/')
496 path = path.strip('/')
486 os_path = self._get_os_path(path=path)
497 os_path = self._get_os_path(path=path)
487 return os.path.exists(os_path)
498 return os.path.exists(os_path)
488
499
489 def _base_model(self, path):
500 def _base_model(self, path):
490 """Build the common base of a contents model"""
501 """Build the common base of a contents model"""
491 os_path = self._get_os_path(path)
502 os_path = self._get_os_path(path)
492 info = os.stat(os_path)
503 info = os.stat(os_path)
493 last_modified = tz.utcfromtimestamp(info.st_mtime)
504 last_modified = tz.utcfromtimestamp(info.st_mtime)
494 created = tz.utcfromtimestamp(info.st_ctime)
505 created = tz.utcfromtimestamp(info.st_ctime)
495 # Create the base model.
506 # Create the base model.
496 model = {}
507 model = {}
497 model['name'] = path.rsplit('/', 1)[-1]
508 model['name'] = path.rsplit('/', 1)[-1]
498 model['path'] = path
509 model['path'] = path
499 model['last_modified'] = last_modified
510 model['last_modified'] = last_modified
500 model['created'] = created
511 model['created'] = created
501 model['content'] = None
512 model['content'] = None
502 model['format'] = None
513 model['format'] = None
503 model['mimetype'] = None
514 model['mimetype'] = None
504 try:
515 try:
505 model['writable'] = os.access(os_path, os.W_OK)
516 model['writable'] = os.access(os_path, os.W_OK)
506 except OSError:
517 except OSError:
507 self.log.error("Failed to check write permissions on %s", os_path)
518 self.log.error("Failed to check write permissions on %s", os_path)
508 model['writable'] = False
519 model['writable'] = False
509 return model
520 return model
510
521
511 def _dir_model(self, path, content=True):
522 def _dir_model(self, path, content=True):
512 """Build a model for a directory
523 """Build a model for a directory
513
524
514 if content is requested, will include a listing of the directory
525 if content is requested, will include a listing of the directory
515 """
526 """
516 os_path = self._get_os_path(path)
527 os_path = self._get_os_path(path)
517
528
518 four_o_four = u'directory does not exist: %r' % path
529 four_o_four = u'directory does not exist: %r' % path
519
530
520 if not os.path.isdir(os_path):
531 if not os.path.isdir(os_path):
521 raise web.HTTPError(404, four_o_four)
532 raise web.HTTPError(404, four_o_four)
522 elif is_hidden(os_path, self.root_dir):
533 elif is_hidden(os_path, self.root_dir):
523 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
534 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
524 os_path
535 os_path
525 )
536 )
526 raise web.HTTPError(404, four_o_four)
537 raise web.HTTPError(404, four_o_four)
527
538
528 model = self._base_model(path)
539 model = self._base_model(path)
529 model['type'] = 'directory'
540 model['type'] = 'directory'
530 if content:
541 if content:
531 model['content'] = contents = []
542 model['content'] = contents = []
532 os_dir = self._get_os_path(path)
543 os_dir = self._get_os_path(path)
533 for name in os.listdir(os_dir):
544 for name in os.listdir(os_dir):
534 os_path = os.path.join(os_dir, name)
545 os_path = os.path.join(os_dir, name)
535 # skip over broken symlinks in listing
546 # skip over broken symlinks in listing
536 if not os.path.exists(os_path):
547 if not os.path.exists(os_path):
537 self.log.warn("%s doesn't exist", os_path)
548 self.log.warn("%s doesn't exist", os_path)
538 continue
549 continue
539 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
550 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
540 self.log.debug("%s not a regular file", os_path)
551 self.log.debug("%s not a regular file", os_path)
541 continue
552 continue
542 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
553 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
543 contents.append(self.get(
554 contents.append(self.get(
544 path='%s/%s' % (path, name),
555 path='%s/%s' % (path, name),
545 content=False)
556 content=False)
546 )
557 )
547
558
548 model['format'] = 'json'
559 model['format'] = 'json'
549
560
550 return model
561 return model
551
562
552 def _file_model(self, path, content=True, format=None):
563 def _file_model(self, path, content=True, format=None):
553 """Build a model for a file
564 """Build a model for a file
554
565
555 if content is requested, include the file contents.
566 if content is requested, include the file contents.
556
567
557 format:
568 format:
558 If 'text', the contents will be decoded as UTF-8.
569 If 'text', the contents will be decoded as UTF-8.
559 If 'base64', the raw bytes contents will be encoded as base64.
570 If 'base64', the raw bytes contents will be encoded as base64.
560 If not specified, try to decode as UTF-8, and fall back to base64
571 If not specified, try to decode as UTF-8, and fall back to base64
561 """
572 """
562 model = self._base_model(path)
573 model = self._base_model(path)
563 model['type'] = 'file'
574 model['type'] = 'file'
564
575
565 os_path = self._get_os_path(path)
576 os_path = self._get_os_path(path)
566
577
567 if content:
578 if content:
568 content, format = self._read_file(os_path, format)
579 content, format = self._read_file(os_path, format)
569 default_mime = {
580 default_mime = {
570 'text': 'text/plain',
581 'text': 'text/plain',
571 'base64': 'application/octet-stream'
582 'base64': 'application/octet-stream'
572 }[format]
583 }[format]
573
584
574 model.update(
585 model.update(
575 content=content,
586 content=content,
576 format=format,
587 format=format,
577 mimetype=mimetypes.guess_type(os_path)[0] or default_mime,
588 mimetype=mimetypes.guess_type(os_path)[0] or default_mime,
578 )
589 )
579
590
580 return model
591 return model
581
592
582 def _notebook_model(self, path, content=True):
593 def _notebook_model(self, path, content=True):
583 """Build a notebook model
594 """Build a notebook model
584
595
585 if content is requested, the notebook content will be populated
596 if content is requested, the notebook content will be populated
586 as a JSON structure (not double-serialized)
597 as a JSON structure (not double-serialized)
587 """
598 """
588 model = self._base_model(path)
599 model = self._base_model(path)
589 model['type'] = 'notebook'
600 model['type'] = 'notebook'
590 if content:
601 if content:
591 os_path = self._get_os_path(path)
602 os_path = self._get_os_path(path)
592 nb = self._read_notebook(os_path, as_version=4)
603 nb = self._read_notebook(os_path, as_version=4)
593 self.mark_trusted_cells(nb, path)
604 self.mark_trusted_cells(nb, path)
594 model['content'] = nb
605 model['content'] = nb
595 model['format'] = 'json'
606 model['format'] = 'json'
596 self.validate_notebook_model(model)
607 self.validate_notebook_model(model)
597 return model
608 return model
598
609
599 def get(self, path, content=True, type=None, format=None):
610 def get(self, path, content=True, type=None, format=None):
600 """ Takes a path for an entity and returns its model
611 """ Takes a path for an entity and returns its model
601
612
602 Parameters
613 Parameters
603 ----------
614 ----------
604 path : str
615 path : str
605 the API path that describes the relative path for the target
616 the API path that describes the relative path for the target
606 content : bool
617 content : bool
607 Whether to include the contents in the reply
618 Whether to include the contents in the reply
608 type : str, optional
619 type : str, optional
609 The requested type - 'file', 'notebook', or 'directory'.
620 The requested type - 'file', 'notebook', or 'directory'.
610 Will raise HTTPError 400 if the content doesn't match.
621 Will raise HTTPError 400 if the content doesn't match.
611 format : str, optional
622 format : str, optional
612 The requested format for file contents. 'text' or 'base64'.
623 The requested format for file contents. 'text' or 'base64'.
613 Ignored if this returns a notebook or directory model.
624 Ignored if this returns a notebook or directory model.
614
625
615 Returns
626 Returns
616 -------
627 -------
617 model : dict
628 model : dict
618 the contents model. If content=True, returns the contents
629 the contents model. If content=True, returns the contents
619 of the file or directory as well.
630 of the file or directory as well.
620 """
631 """
621 path = path.strip('/')
632 path = path.strip('/')
622
633
623 if not self.exists(path):
634 if not self.exists(path):
624 raise web.HTTPError(404, u'No such file or directory: %s' % path)
635 raise web.HTTPError(404, u'No such file or directory: %s' % path)
625
636
626 os_path = self._get_os_path(path)
637 os_path = self._get_os_path(path)
627 if os.path.isdir(os_path):
638 if os.path.isdir(os_path):
628 if type not in (None, 'directory'):
639 if type not in (None, 'directory'):
629 raise web.HTTPError(400,
640 raise web.HTTPError(400,
630 u'%s is a directory, not a %s' % (path, type), reason='bad type')
641 u'%s is a directory, not a %s' % (path, type), reason='bad type')
631 model = self._dir_model(path, content=content)
642 model = self._dir_model(path, content=content)
632 elif type == 'notebook' or (type is None and path.endswith('.ipynb')):
643 elif type == 'notebook' or (type is None and path.endswith('.ipynb')):
633 model = self._notebook_model(path, content=content)
644 model = self._notebook_model(path, content=content)
634 else:
645 else:
635 if type == 'directory':
646 if type == 'directory':
636 raise web.HTTPError(400,
647 raise web.HTTPError(400,
637 u'%s is not a directory', reason='bad type')
648 u'%s is not a directory', reason='bad type')
638 model = self._file_model(path, content=content, format=format)
649 model = self._file_model(path, content=content, format=format)
639 return model
650 return model
640
651
641 def _save_directory(self, os_path, model, path=''):
652 def _save_directory(self, os_path, model, path=''):
642 """create a directory"""
653 """create a directory"""
643 if is_hidden(os_path, self.root_dir):
654 if is_hidden(os_path, self.root_dir):
644 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
655 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
645 if not os.path.exists(os_path):
656 if not os.path.exists(os_path):
646 with self.perm_to_403():
657 with self.perm_to_403():
647 os.mkdir(os_path)
658 os.mkdir(os_path)
648 elif not os.path.isdir(os_path):
659 elif not os.path.isdir(os_path):
649 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
660 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
650 else:
661 else:
651 self.log.debug("Directory %r already exists", os_path)
662 self.log.debug("Directory %r already exists", os_path)
652
663
653 def save(self, model, path=''):
664 def save(self, model, path=''):
654 """Save the file model and return the model with no content."""
665 """Save the file model and return the model with no content."""
655 path = path.strip('/')
666 path = path.strip('/')
656
667
657 if 'type' not in model:
668 if 'type' not in model:
658 raise web.HTTPError(400, u'No file type provided')
669 raise web.HTTPError(400, u'No file type provided')
659 if 'content' not in model and model['type'] != 'directory':
670 if 'content' not in model and model['type'] != 'directory':
660 raise web.HTTPError(400, u'No file content provided')
671 raise web.HTTPError(400, u'No file content provided')
661
672
662 self.run_pre_save_hook(model=model, path=path)
673 self.run_pre_save_hook(model=model, path=path)
663
674
664 os_path = self._get_os_path(path)
675 os_path = self._get_os_path(path)
665 self.log.debug("Saving %s", os_path)
676 self.log.debug("Saving %s", os_path)
666 try:
677 try:
667 if model['type'] == 'notebook':
678 if model['type'] == 'notebook':
668 nb = nbformat.from_dict(model['content'])
679 nb = nbformat.from_dict(model['content'])
669 self.check_and_sign(nb, path)
680 self.check_and_sign(nb, path)
670 self._save_notebook(os_path, nb)
681 self._save_notebook(os_path, nb)
671 # One checkpoint should always exist for notebooks.
682 # One checkpoint should always exist for notebooks.
672 if not self.checkpoint_manager.list_checkpoints(path):
683 if not self.checkpoint_manager.list_checkpoints(path):
673 self.checkpoint_manager.create_notebook_checkpoint(
684 self.checkpoint_manager.create_notebook_checkpoint(
674 nb,
685 nb,
675 path,
686 path,
676 )
687 )
677 elif model['type'] == 'file':
688 elif model['type'] == 'file':
678 # Missing format will be handled internally by _save_file.
689 # Missing format will be handled internally by _save_file.
679 self._save_file(os_path, model['content'], model.get('format'))
690 self._save_file(os_path, model['content'], model.get('format'))
680 elif model['type'] == 'directory':
691 elif model['type'] == 'directory':
681 self._save_directory(os_path, model, path)
692 self._save_directory(os_path, model, path)
682 else:
693 else:
683 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
694 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
684 except web.HTTPError:
695 except web.HTTPError:
685 raise
696 raise
686 except Exception as e:
697 except Exception as e:
687 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
698 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
688 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
699 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
689
700
690 validation_message = None
701 validation_message = None
691 if model['type'] == 'notebook':
702 if model['type'] == 'notebook':
692 self.validate_notebook_model(model)
703 self.validate_notebook_model(model)
693 validation_message = model.get('message', None)
704 validation_message = model.get('message', None)
694
705
695 model = self.get(path, content=False)
706 model = self.get(path, content=False)
696 if validation_message:
707 if validation_message:
697 model['message'] = validation_message
708 model['message'] = validation_message
698
709
699 self.run_post_save_hook(model=model, os_path=os_path)
710 self.run_post_save_hook(model=model, os_path=os_path)
700
711
701 return model
712 return model
702
713
703 def delete_file(self, path):
714 def delete_file(self, path):
704 """Delete file at path."""
715 """Delete file at path."""
705 path = path.strip('/')
716 path = path.strip('/')
706 os_path = self._get_os_path(path)
717 os_path = self._get_os_path(path)
707 rm = os.unlink
718 rm = os.unlink
708 if os.path.isdir(os_path):
719 if os.path.isdir(os_path):
709 listing = os.listdir(os_path)
720 listing = os.listdir(os_path)
710 # Don't delete non-empty directories.
721 # Don't delete non-empty directories.
711 # A directory containing only leftover checkpoints is
722 # A directory containing only leftover checkpoints is
712 # considered empty.
723 # considered empty.
713 cp_dir = getattr(self.checkpoint_manager, 'checkpoint_dir', None)
724 cp_dir = getattr(self.checkpoint_manager, 'checkpoint_dir', None)
714 for entry in listing:
725 for entry in listing:
715 if entry != cp_dir:
726 if entry != cp_dir:
716 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
727 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
717 elif not os.path.isfile(os_path):
728 elif not os.path.isfile(os_path):
718 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
729 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
719
730
720 if os.path.isdir(os_path):
731 if os.path.isdir(os_path):
721 self.log.debug("Removing directory %s", os_path)
732 self.log.debug("Removing directory %s", os_path)
722 with self.perm_to_403():
733 with self.perm_to_403():
723 shutil.rmtree(os_path)
734 shutil.rmtree(os_path)
724 else:
735 else:
725 self.log.debug("Unlinking file %s", os_path)
736 self.log.debug("Unlinking file %s", os_path)
726 with self.perm_to_403():
737 with self.perm_to_403():
727 rm(os_path)
738 rm(os_path)
728
739
729 def rename_file(self, old_path, new_path):
740 def rename_file(self, old_path, new_path):
730 """Rename a file."""
741 """Rename a file."""
731 old_path = old_path.strip('/')
742 old_path = old_path.strip('/')
732 new_path = new_path.strip('/')
743 new_path = new_path.strip('/')
733 if new_path == old_path:
744 if new_path == old_path:
734 return
745 return
735
746
736 new_os_path = self._get_os_path(new_path)
747 new_os_path = self._get_os_path(new_path)
737 old_os_path = self._get_os_path(old_path)
748 old_os_path = self._get_os_path(old_path)
738
749
739 # Should we proceed with the move?
750 # Should we proceed with the move?
740 if os.path.exists(new_os_path):
751 if os.path.exists(new_os_path):
741 raise web.HTTPError(409, u'File already exists: %s' % new_path)
752 raise web.HTTPError(409, u'File already exists: %s' % new_path)
742
753
743 # Move the file
754 # Move the file
744 try:
755 try:
745 with self.perm_to_403():
756 with self.perm_to_403():
746 shutil.move(old_os_path, new_os_path)
757 shutil.move(old_os_path, new_os_path)
747 except web.HTTPError:
758 except web.HTTPError:
748 raise
759 raise
749 except Exception as e:
760 except Exception as e:
750 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
761 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
751
762
752 def info_string(self):
763 def info_string(self):
753 return "Serving notebooks from local directory: %s" % self.root_dir
764 return "Serving notebooks from local directory: %s" % self.root_dir
754
765
755 def get_kernel_path(self, path, model=None):
766 def get_kernel_path(self, path, model=None):
756 """Return the initial API path of a kernel associated with a given notebook"""
767 """Return the initial API path of a kernel associated with a given notebook"""
757 if '/' in path:
768 if '/' in path:
758 parent_dir = path.rsplit('/', 1)[0]
769 parent_dir = path.rsplit('/', 1)[0]
759 else:
770 else:
760 parent_dir = ''
771 parent_dir = ''
761 return parent_dir
772 return parent_dir
@@ -1,541 +1,536 b''
1 """A base class for contents managers."""
1 """A base class for contents managers."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from fnmatch import fnmatch
6 from fnmatch import fnmatch
7 import itertools
7 import itertools
8 import json
8 import json
9 import os
9 import os
10 import re
10 import re
11
11
12 from tornado.web import HTTPError
12 from tornado.web import HTTPError
13
13
14 from IPython import nbformat
14 from IPython import nbformat
15 from IPython.config.configurable import LoggingConfigurable
15 from IPython.config.configurable import LoggingConfigurable
16 from IPython.nbformat import sign, validate, ValidationError
16 from IPython.nbformat import sign, validate, ValidationError
17 from IPython.nbformat.v4 import new_notebook
17 from IPython.nbformat.v4 import new_notebook
18 from IPython.utils.importstring import import_item
18 from IPython.utils.importstring import import_item
19 from IPython.utils.traitlets import (
19 from IPython.utils.traitlets import (
20 Any,
20 Any,
21 Dict,
21 Dict,
22 Instance,
22 Instance,
23 List,
23 List,
24 TraitError,
24 TraitError,
25 Type,
25 Type,
26 Unicode,
26 Unicode,
27 )
27 )
28 from IPython.utils.py3compat import string_types
28 from IPython.utils.py3compat import string_types
29
29
30 copy_pat = re.compile(r'\-Copy\d*\.')
30 copy_pat = re.compile(r'\-Copy\d*\.')
31
31
32
32
33 class CheckpointManager(LoggingConfigurable):
33 class CheckpointManager(LoggingConfigurable):
34 """
34 """
35 Base class for managing checkpoints for a ContentsManager.
35 Base class for managing checkpoints for a ContentsManager.
36 """
36 """
37 def create_file_checkpoint(self, content, format, path):
37 def create_file_checkpoint(self, content, format, path):
38 """Create a checkpoint of the current state of a file
38 """Create a checkpoint of the current state of a file
39
39
40 Returns a checkpoint model for the new checkpoint.
40 Returns a checkpoint model for the new checkpoint.
41 """
41 """
42 raise NotImplementedError("must be implemented in a subclass")
42 raise NotImplementedError("must be implemented in a subclass")
43
43
44 def create_notebook_checkpoint(self, nb, path):
44 def create_notebook_checkpoint(self, nb, path):
45 """Create a checkpoint of the current state of a file
45 """Create a checkpoint of the current state of a file
46
46
47 Returns a checkpoint model for the new checkpoint.
47 Returns a checkpoint model for the new checkpoint.
48 """
48 """
49 raise NotImplementedError("must be implemented in a subclass")
49 raise NotImplementedError("must be implemented in a subclass")
50
50
51 def get_checkpoint(self, checkpoint_id, path, type):
51 def get_checkpoint(self, checkpoint_id, path, type):
52 """Get the content of a checkpoint.
52 """Get the content of a checkpoint.
53
53
54 Returns an unvalidated model with the same structure as
54 Returns an unvalidated model with the same structure as
55 the return value of ContentsManager.get
55 the return value of ContentsManager.get
56 """
56 """
57 raise NotImplementedError("must be implemented in a subclass")
57 raise NotImplementedError("must be implemented in a subclass")
58
58
59 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
59 def rename_checkpoint(self, checkpoint_id, old_path, new_path):
60 """Rename a single checkpoint from old_path to new_path."""
60 """Rename a single checkpoint from old_path to new_path."""
61 raise NotImplementedError("must be implemented in a subclass")
61 raise NotImplementedError("must be implemented in a subclass")
62
62
63 def delete_checkpoint(self, checkpoint_id, path):
63 def delete_checkpoint(self, checkpoint_id, path):
64 """delete a checkpoint for a file"""
64 """delete a checkpoint for a file"""
65 raise NotImplementedError("must be implemented in a subclass")
65 raise NotImplementedError("must be implemented in a subclass")
66
66
67 def list_checkpoints(self, path):
67 def list_checkpoints(self, path):
68 """Return a list of checkpoints for a given file"""
68 """Return a list of checkpoints for a given file"""
69 raise NotImplementedError("must be implemented in a subclass")
69 raise NotImplementedError("must be implemented in a subclass")
70
70
71 def rename_all_checkpoints(self, old_path, new_path):
71 def rename_all_checkpoints(self, old_path, new_path):
72 """Rename all checkpoints for old_path to new_path."""
72 """Rename all checkpoints for old_path to new_path."""
73 for cp in self.list_checkpoints(old_path):
73 for cp in self.list_checkpoints(old_path):
74 self.rename_checkpoint(cp['id'], old_path, new_path)
74 self.rename_checkpoint(cp['id'], old_path, new_path)
75
75
76 def delete_all_checkpoints(self, path):
76 def delete_all_checkpoints(self, path):
77 """Delete all checkpoints for the given path."""
77 """Delete all checkpoints for the given path."""
78 for checkpoint in self.list_checkpoints(path):
78 for checkpoint in self.list_checkpoints(path):
79 self.delete_checkpoint(checkpoint['id'], path)
79 self.delete_checkpoint(checkpoint['id'], path)
80
80
81
81
82 class ContentsManager(LoggingConfigurable):
82 class ContentsManager(LoggingConfigurable):
83 """Base class for serving files and directories.
83 """Base class for serving files and directories.
84
84
85 This serves any text or binary file,
85 This serves any text or binary file,
86 as well as directories,
86 as well as directories,
87 with special handling for JSON notebook documents.
87 with special handling for JSON notebook documents.
88
88
89 Most APIs take a path argument,
89 Most APIs take a path argument,
90 which is always an API-style unicode path,
90 which is always an API-style unicode path,
91 and always refers to a directory.
91 and always refers to a directory.
92
92
93 - unicode, not url-escaped
93 - unicode, not url-escaped
94 - '/'-separated
94 - '/'-separated
95 - leading and trailing '/' will be stripped
95 - leading and trailing '/' will be stripped
96 - if unspecified, path defaults to '',
96 - if unspecified, path defaults to '',
97 indicating the root path.
97 indicating the root path.
98
98
99 """
99 """
100
100
101 notary = Instance(sign.NotebookNotary)
101 notary = Instance(sign.NotebookNotary)
102 def _notary_default(self):
102 def _notary_default(self):
103 return sign.NotebookNotary(parent=self)
103 return sign.NotebookNotary(parent=self)
104
104
105 hide_globs = List(Unicode, [
105 hide_globs = List(Unicode, [
106 u'__pycache__', '*.pyc', '*.pyo',
106 u'__pycache__', '*.pyc', '*.pyo',
107 '.DS_Store', '*.so', '*.dylib', '*~',
107 '.DS_Store', '*.so', '*.dylib', '*~',
108 ], config=True, help="""
108 ], config=True, help="""
109 Glob patterns to hide in file and directory listings.
109 Glob patterns to hide in file and directory listings.
110 """)
110 """)
111
111
112 untitled_notebook = Unicode("Untitled", config=True,
112 untitled_notebook = Unicode("Untitled", config=True,
113 help="The base name used when creating untitled notebooks."
113 help="The base name used when creating untitled notebooks."
114 )
114 )
115
115
116 untitled_file = Unicode("untitled", config=True,
116 untitled_file = Unicode("untitled", config=True,
117 help="The base name used when creating untitled files."
117 help="The base name used when creating untitled files."
118 )
118 )
119
119
120 untitled_directory = Unicode("Untitled Folder", config=True,
120 untitled_directory = Unicode("Untitled Folder", config=True,
121 help="The base name used when creating untitled directories."
121 help="The base name used when creating untitled directories."
122 )
122 )
123
123
124 pre_save_hook = Any(None, config=True,
124 pre_save_hook = Any(None, config=True,
125 help="""Python callable or importstring thereof
125 help="""Python callable or importstring thereof
126
126
127 To be called on a contents model prior to save.
127 To be called on a contents model prior to save.
128
128
129 This can be used to process the structure,
129 This can be used to process the structure,
130 such as removing notebook outputs or other side effects that
130 such as removing notebook outputs or other side effects that
131 should not be saved.
131 should not be saved.
132
132
133 It will be called as (all arguments passed by keyword):
133 It will be called as (all arguments passed by keyword):
134
134
135 hook(path=path, model=model, contents_manager=self)
135 hook(path=path, model=model, contents_manager=self)
136
136
137 model: the model to be saved. Includes file contents.
137 model: the model to be saved. Includes file contents.
138 modifying this dict will affect the file that is stored.
138 modifying this dict will affect the file that is stored.
139 path: the API path of the save destination
139 path: the API path of the save destination
140 contents_manager: this ContentsManager instance
140 contents_manager: this ContentsManager instance
141 """
141 """
142 )
142 )
143 def _pre_save_hook_changed(self, name, old, new):
143 def _pre_save_hook_changed(self, name, old, new):
144 if new and isinstance(new, string_types):
144 if new and isinstance(new, string_types):
145 self.pre_save_hook = import_item(self.pre_save_hook)
145 self.pre_save_hook = import_item(self.pre_save_hook)
146 elif new:
146 elif new:
147 if not callable(new):
147 if not callable(new):
148 raise TraitError("pre_save_hook must be callable")
148 raise TraitError("pre_save_hook must be callable")
149
149
150 def run_pre_save_hook(self, model, path, **kwargs):
150 def run_pre_save_hook(self, model, path, **kwargs):
151 """Run the pre-save hook if defined, and log errors"""
151 """Run the pre-save hook if defined, and log errors"""
152 if self.pre_save_hook:
152 if self.pre_save_hook:
153 try:
153 try:
154 self.log.debug("Running pre-save hook on %s", path)
154 self.log.debug("Running pre-save hook on %s", path)
155 self.pre_save_hook(model=model, path=path, contents_manager=self, **kwargs)
155 self.pre_save_hook(model=model, path=path, contents_manager=self, **kwargs)
156 except Exception:
156 except Exception:
157 self.log.error("Pre-save hook failed on %s", path, exc_info=True)
157 self.log.error("Pre-save hook failed on %s", path, exc_info=True)
158
158
159 checkpoint_manager_class = Type(CheckpointManager, config=True)
159 checkpoint_manager_class = Type(CheckpointManager, config=True)
160 checkpoint_manager = Instance(CheckpointManager, config=True)
160 checkpoint_manager = Instance(CheckpointManager, config=True)
161 checkpoint_manager_kwargs = Dict(allow_none=False, config=True)
161 checkpoint_manager_kwargs = Dict(allow_none=False, config=True)
162
162
163 def _checkpoint_manager_default(self):
163 def _checkpoint_manager_default(self):
164 return self.checkpoint_manager_class(**self.checkpoint_manager_kwargs)
164 return self.checkpoint_manager_class(**self.checkpoint_manager_kwargs)
165
165
166 def _checkpoint_manager_kwargs_default(self):
166 def _checkpoint_manager_kwargs_default(self):
167 return dict(
167 return dict(
168 parent=self,
168 parent=self,
169 log=self.log,
169 log=self.log,
170 )
170 )
171
171
172 # ContentsManager API part 1: methods that must be
172 # ContentsManager API part 1: methods that must be
173 # implemented in subclasses.
173 # implemented in subclasses.
174
174
175 def dir_exists(self, path):
175 def dir_exists(self, path):
176 """Does the API-style path (directory) actually exist?
176 """Does the API-style path (directory) actually exist?
177
177
178 Like os.path.isdir
178 Like os.path.isdir
179
179
180 Override this method in subclasses.
180 Override this method in subclasses.
181
181
182 Parameters
182 Parameters
183 ----------
183 ----------
184 path : string
184 path : string
185 The path to check
185 The path to check
186
186
187 Returns
187 Returns
188 -------
188 -------
189 exists : bool
189 exists : bool
190 Whether the path does indeed exist.
190 Whether the path does indeed exist.
191 """
191 """
192 raise NotImplementedError
192 raise NotImplementedError
193
193
194 def is_hidden(self, path):
194 def is_hidden(self, path):
195 """Does the API style path correspond to a hidden directory or file?
195 """Does the API style path correspond to a hidden directory or file?
196
196
197 Parameters
197 Parameters
198 ----------
198 ----------
199 path : string
199 path : string
200 The path to check. This is an API path (`/` separated,
200 The path to check. This is an API path (`/` separated,
201 relative to root dir).
201 relative to root dir).
202
202
203 Returns
203 Returns
204 -------
204 -------
205 hidden : bool
205 hidden : bool
206 Whether the path is hidden.
206 Whether the path is hidden.
207
207
208 """
208 """
209 raise NotImplementedError
209 raise NotImplementedError
210
210
211 def file_exists(self, path=''):
211 def file_exists(self, path=''):
212 """Does a file exist at the given path?
212 """Does a file exist at the given path?
213
213
214 Like os.path.isfile
214 Like os.path.isfile
215
215
216 Override this method in subclasses.
216 Override this method in subclasses.
217
217
218 Parameters
218 Parameters
219 ----------
219 ----------
220 name : string
220 name : string
221 The name of the file you are checking.
221 The name of the file you are checking.
222 path : string
222 path : string
223 The relative path to the file's directory (with '/' as separator)
223 The relative path to the file's directory (with '/' as separator)
224
224
225 Returns
225 Returns
226 -------
226 -------
227 exists : bool
227 exists : bool
228 Whether the file exists.
228 Whether the file exists.
229 """
229 """
230 raise NotImplementedError('must be implemented in a subclass')
230 raise NotImplementedError('must be implemented in a subclass')
231
231
232 def exists(self, path):
232 def exists(self, path):
233 """Does a file or directory exist at the given path?
233 """Does a file or directory exist at the given path?
234
234
235 Like os.path.exists
235 Like os.path.exists
236
236
237 Parameters
237 Parameters
238 ----------
238 ----------
239 path : string
239 path : string
240 The relative path to the file's directory (with '/' as separator)
240 The relative path to the file's directory (with '/' as separator)
241
241
242 Returns
242 Returns
243 -------
243 -------
244 exists : bool
244 exists : bool
245 Whether the target exists.
245 Whether the target exists.
246 """
246 """
247 return self.file_exists(path) or self.dir_exists(path)
247 return self.file_exists(path) or self.dir_exists(path)
248
248
249 def get(self, path, content=True, type=None, format=None):
249 def get(self, path, content=True, type=None, format=None):
250 """Get the model of a file or directory with or without content."""
250 """Get the model of a file or directory with or without content."""
251 raise NotImplementedError('must be implemented in a subclass')
251 raise NotImplementedError('must be implemented in a subclass')
252
252
253 def save(self, model, path):
253 def save(self, model, path):
254 """Save the file or directory and return the model with no content.
254 """Save the file or directory and return the model with no content.
255
255
256 Save implementations should call self.run_pre_save_hook(model=model, path=path)
256 Save implementations should call self.run_pre_save_hook(model=model, path=path)
257 prior to writing any data.
257 prior to writing any data.
258 """
258 """
259 raise NotImplementedError('must be implemented in a subclass')
259 raise NotImplementedError('must be implemented in a subclass')
260
260
261 def delete_file(self, path):
261 def delete_file(self, path):
262 """Delete file or directory by path."""
262 """Delete file or directory by path."""
263 raise NotImplementedError('must be implemented in a subclass')
263 raise NotImplementedError('must be implemented in a subclass')
264
264
265 def rename_file(self, old_path, new_path):
265 def rename_file(self, old_path, new_path):
266 """Rename a file."""
266 """Rename a file."""
267 raise NotImplementedError('must be implemented in a subclass')
267 raise NotImplementedError('must be implemented in a subclass')
268
268
269 # ContentsManager API part 2: methods that have useable default
269 # ContentsManager API part 2: methods that have useable default
270 # implementations, but can be overridden in subclasses.
270 # implementations, but can be overridden in subclasses.
271
271
272 def delete(self, path):
272 def delete(self, path):
273 """Delete a file/directory and any associated checkpoints."""
273 """Delete a file/directory and any associated checkpoints."""
274 self.delete_file(path)
274 self.delete_file(path)
275 self.checkpoint_manager.delete_all_checkpoints(path)
275 self.checkpoint_manager.delete_all_checkpoints(path)
276
276
277 def rename(self, old_path, new_path):
277 def rename(self, old_path, new_path):
278 """Rename a file and any checkpoints associated with that file."""
278 """Rename a file and any checkpoints associated with that file."""
279 self.rename_file(old_path, new_path)
279 self.rename_file(old_path, new_path)
280 self.checkpoint_manager.rename_all_checkpoints(old_path, new_path)
280 self.checkpoint_manager.rename_all_checkpoints(old_path, new_path)
281
281
282 def update(self, model, path):
282 def update(self, model, path):
283 """Update the file's path
283 """Update the file's path
284
284
285 For use in PATCH requests, to enable renaming a file without
285 For use in PATCH requests, to enable renaming a file without
286 re-uploading its contents. Only used for renaming at the moment.
286 re-uploading its contents. Only used for renaming at the moment.
287 """
287 """
288 path = path.strip('/')
288 path = path.strip('/')
289 new_path = model.get('path', path).strip('/')
289 new_path = model.get('path', path).strip('/')
290 if path != new_path:
290 if path != new_path:
291 self.rename(path, new_path)
291 self.rename(path, new_path)
292 model = self.get(new_path, content=False)
292 model = self.get(new_path, content=False)
293 return model
293 return model
294
294
295 def info_string(self):
295 def info_string(self):
296 return "Serving contents"
296 return "Serving contents"
297
297
298 def get_kernel_path(self, path, model=None):
298 def get_kernel_path(self, path, model=None):
299 """Return the API path for the kernel
299 """Return the API path for the kernel
300
300
301 KernelManagers can turn this value into a filesystem path,
301 KernelManagers can turn this value into a filesystem path,
302 or ignore it altogether.
302 or ignore it altogether.
303
303
304 The default value here will start kernels in the directory of the
304 The default value here will start kernels in the directory of the
305 notebook server. FileContentsManager overrides this to use the
305 notebook server. FileContentsManager overrides this to use the
306 directory containing the notebook.
306 directory containing the notebook.
307 """
307 """
308 return ''
308 return ''
309
309
310 def increment_filename(self, filename, path='', insert=''):
310 def increment_filename(self, filename, path='', insert=''):
311 """Increment a filename until it is unique.
311 """Increment a filename until it is unique.
312
312
313 Parameters
313 Parameters
314 ----------
314 ----------
315 filename : unicode
315 filename : unicode
316 The name of a file, including extension
316 The name of a file, including extension
317 path : unicode
317 path : unicode
318 The API path of the target's directory
318 The API path of the target's directory
319
319
320 Returns
320 Returns
321 -------
321 -------
322 name : unicode
322 name : unicode
323 A filename that is unique, based on the input filename.
323 A filename that is unique, based on the input filename.
324 """
324 """
325 path = path.strip('/')
325 path = path.strip('/')
326 basename, ext = os.path.splitext(filename)
326 basename, ext = os.path.splitext(filename)
327 for i in itertools.count():
327 for i in itertools.count():
328 if i:
328 if i:
329 insert_i = '{}{}'.format(insert, i)
329 insert_i = '{}{}'.format(insert, i)
330 else:
330 else:
331 insert_i = ''
331 insert_i = ''
332 name = u'{basename}{insert}{ext}'.format(basename=basename,
332 name = u'{basename}{insert}{ext}'.format(basename=basename,
333 insert=insert_i, ext=ext)
333 insert=insert_i, ext=ext)
334 if not self.exists(u'{}/{}'.format(path, name)):
334 if not self.exists(u'{}/{}'.format(path, name)):
335 break
335 break
336 return name
336 return name
337
337
338 def validate_notebook_model(self, model):
338 def validate_notebook_model(self, model):
339 """Add failed-validation message to model"""
339 """Add failed-validation message to model"""
340 try:
340 try:
341 validate(model['content'])
341 validate(model['content'])
342 except ValidationError as e:
342 except ValidationError as e:
343 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
343 model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
344 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
344 e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
345 )
345 )
346 return model
346 return model
347
347
348 def new_untitled(self, path='', type='', ext=''):
348 def new_untitled(self, path='', type='', ext=''):
349 """Create a new untitled file or directory in path
349 """Create a new untitled file or directory in path
350
350
351 path must be a directory
351 path must be a directory
352
352
353 File extension can be specified.
353 File extension can be specified.
354
354
355 Use `new` to create files with a fully specified path (including filename).
355 Use `new` to create files with a fully specified path (including filename).
356 """
356 """
357 path = path.strip('/')
357 path = path.strip('/')
358 if not self.dir_exists(path):
358 if not self.dir_exists(path):
359 raise HTTPError(404, 'No such directory: %s' % path)
359 raise HTTPError(404, 'No such directory: %s' % path)
360
360
361 model = {}
361 model = {}
362 if type:
362 if type:
363 model['type'] = type
363 model['type'] = type
364
364
365 if ext == '.ipynb':
365 if ext == '.ipynb':
366 model.setdefault('type', 'notebook')
366 model.setdefault('type', 'notebook')
367 else:
367 else:
368 model.setdefault('type', 'file')
368 model.setdefault('type', 'file')
369
369
370 insert = ''
370 insert = ''
371 if model['type'] == 'directory':
371 if model['type'] == 'directory':
372 untitled = self.untitled_directory
372 untitled = self.untitled_directory
373 insert = ' '
373 insert = ' '
374 elif model['type'] == 'notebook':
374 elif model['type'] == 'notebook':
375 untitled = self.untitled_notebook
375 untitled = self.untitled_notebook
376 ext = '.ipynb'
376 ext = '.ipynb'
377 elif model['type'] == 'file':
377 elif model['type'] == 'file':
378 untitled = self.untitled_file
378 untitled = self.untitled_file
379 else:
379 else:
380 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
380 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
381
381
382 name = self.increment_filename(untitled + ext, path, insert=insert)
382 name = self.increment_filename(untitled + ext, path, insert=insert)
383 path = u'{0}/{1}'.format(path, name)
383 path = u'{0}/{1}'.format(path, name)
384 return self.new(model, path)
384 return self.new(model, path)
385
385
386 def new(self, model=None, path=''):
386 def new(self, model=None, path=''):
387 """Create a new file or directory and return its model with no content.
387 """Create a new file or directory and return its model with no content.
388
388
389 To create a new untitled entity in a directory, use `new_untitled`.
389 To create a new untitled entity in a directory, use `new_untitled`.
390 """
390 """
391 path = path.strip('/')
391 path = path.strip('/')
392 if model is None:
392 if model is None:
393 model = {}
393 model = {}
394
394
395 if path.endswith('.ipynb'):
395 if path.endswith('.ipynb'):
396 model.setdefault('type', 'notebook')
396 model.setdefault('type', 'notebook')
397 else:
397 else:
398 model.setdefault('type', 'file')
398 model.setdefault('type', 'file')
399
399
400 # no content, not a directory, so fill out new-file model
400 # no content, not a directory, so fill out new-file model
401 if 'content' not in model and model['type'] != 'directory':
401 if 'content' not in model and model['type'] != 'directory':
402 if model['type'] == 'notebook':
402 if model['type'] == 'notebook':
403 model['content'] = new_notebook()
403 model['content'] = new_notebook()
404 model['format'] = 'json'
404 model['format'] = 'json'
405 else:
405 else:
406 model['content'] = ''
406 model['content'] = ''
407 model['type'] = 'file'
407 model['type'] = 'file'
408 model['format'] = 'text'
408 model['format'] = 'text'
409
409
410 model = self.save(model, path)
410 model = self.save(model, path)
411 return model
411 return model
412
412
413 def copy(self, from_path, to_path=None):
413 def copy(self, from_path, to_path=None):
414 """Copy an existing file and return its new model.
414 """Copy an existing file and return its new model.
415
415
416 If to_path not specified, it will be the parent directory of from_path.
416 If to_path not specified, it will be the parent directory of from_path.
417 If to_path is a directory, filename will increment `from_path-Copy#.ext`.
417 If to_path is a directory, filename will increment `from_path-Copy#.ext`.
418
418
419 from_path must be a full path to a file.
419 from_path must be a full path to a file.
420 """
420 """
421 path = from_path.strip('/')
421 path = from_path.strip('/')
422 if to_path is not None:
422 if to_path is not None:
423 to_path = to_path.strip('/')
423 to_path = to_path.strip('/')
424
424
425 if '/' in path:
425 if '/' in path:
426 from_dir, from_name = path.rsplit('/', 1)
426 from_dir, from_name = path.rsplit('/', 1)
427 else:
427 else:
428 from_dir = ''
428 from_dir = ''
429 from_name = path
429 from_name = path
430
430
431 model = self.get(path)
431 model = self.get(path)
432 model.pop('path', None)
432 model.pop('path', None)
433 model.pop('name', None)
433 model.pop('name', None)
434 if model['type'] == 'directory':
434 if model['type'] == 'directory':
435 raise HTTPError(400, "Can't copy directories")
435 raise HTTPError(400, "Can't copy directories")
436
436
437 if to_path is None:
437 if to_path is None:
438 to_path = from_dir
438 to_path = from_dir
439 if self.dir_exists(to_path):
439 if self.dir_exists(to_path):
440 name = copy_pat.sub(u'.', from_name)
440 name = copy_pat.sub(u'.', from_name)
441 to_name = self.increment_filename(name, to_path, insert='-Copy')
441 to_name = self.increment_filename(name, to_path, insert='-Copy')
442 to_path = u'{0}/{1}'.format(to_path, to_name)
442 to_path = u'{0}/{1}'.format(to_path, to_name)
443
443
444 model = self.save(model, to_path)
444 model = self.save(model, to_path)
445 return model
445 return model
446
446
447 def log_info(self):
447 def log_info(self):
448 self.log.info(self.info_string())
448 self.log.info(self.info_string())
449
449
450 def trust_notebook(self, path):
450 def trust_notebook(self, path):
451 """Explicitly trust a notebook
451 """Explicitly trust a notebook
452
452
453 Parameters
453 Parameters
454 ----------
454 ----------
455 path : string
455 path : string
456 The path of a notebook
456 The path of a notebook
457 """
457 """
458 model = self.get(path)
458 model = self.get(path)
459 nb = model['content']
459 nb = model['content']
460 self.log.warn("Trusting notebook %s", path)
460 self.log.warn("Trusting notebook %s", path)
461 self.notary.mark_cells(nb, True)
461 self.notary.mark_cells(nb, True)
462 self.save(model, path)
462 self.save(model, path)
463
463
464 def check_and_sign(self, nb, path=''):
464 def check_and_sign(self, nb, path=''):
465 """Check for trusted cells, and sign the notebook.
465 """Check for trusted cells, and sign the notebook.
466
466
467 Called as a part of saving notebooks.
467 Called as a part of saving notebooks.
468
468
469 Parameters
469 Parameters
470 ----------
470 ----------
471 nb : dict
471 nb : dict
472 The notebook dict
472 The notebook dict
473 path : string
473 path : string
474 The notebook's path (for logging)
474 The notebook's path (for logging)
475 """
475 """
476 if self.notary.check_cells(nb):
476 if self.notary.check_cells(nb):
477 self.notary.sign(nb)
477 self.notary.sign(nb)
478 else:
478 else:
479 self.log.warn("Saving untrusted notebook %s", path)
479 self.log.warn("Saving untrusted notebook %s", path)
480
480
481 def mark_trusted_cells(self, nb, path=''):
481 def mark_trusted_cells(self, nb, path=''):
482 """Mark cells as trusted if the notebook signature matches.
482 """Mark cells as trusted if the notebook signature matches.
483
483
484 Called as a part of loading notebooks.
484 Called as a part of loading notebooks.
485
485
486 Parameters
486 Parameters
487 ----------
487 ----------
488 nb : dict
488 nb : dict
489 The notebook object (in current nbformat)
489 The notebook object (in current nbformat)
490 path : string
490 path : string
491 The notebook's path (for logging)
491 The notebook's path (for logging)
492 """
492 """
493 trusted = self.notary.check_signature(nb)
493 trusted = self.notary.check_signature(nb)
494 if not trusted:
494 if not trusted:
495 self.log.warn("Notebook %s is not trusted", path)
495 self.log.warn("Notebook %s is not trusted", path)
496 self.notary.mark_cells(nb, trusted)
496 self.notary.mark_cells(nb, trusted)
497
497
498 def should_list(self, name):
498 def should_list(self, name):
499 """Should this file/directory name be displayed in a listing?"""
499 """Should this file/directory name be displayed in a listing?"""
500 return not any(fnmatch(name, glob) for glob in self.hide_globs)
500 return not any(fnmatch(name, glob) for glob in self.hide_globs)
501
501
502 # Part 3: Checkpoints API
502 # Part 3: Checkpoints API
503 def create_checkpoint(self, path):
503 def create_checkpoint(self, path):
504 """Create a checkpoint."""
504 """Create a checkpoint."""
505 model = self.get(path, content=True)
505 model = self.get(path, content=True)
506 type = model['type']
506 type = model['type']
507 if type == 'notebook':
507 if type == 'notebook':
508 return self.checkpoint_manager.create_notebook_checkpoint(
508 return self.checkpoint_manager.create_notebook_checkpoint(
509 model['content'],
509 model['content'],
510 path,
510 path,
511 )
511 )
512 elif type == 'file':
512 elif type == 'file':
513 return self.checkpoint_manager.create_file_checkpoint(
513 return self.checkpoint_manager.create_file_checkpoint(
514 model['content'],
514 model['content'],
515 model['format'],
515 model['format'],
516 path,
516 path,
517 )
517 )
518
518
519 def list_checkpoints(self, path):
519 def list_checkpoints(self, path):
520 return self.checkpoint_manager.list_checkpoints(path)
520 return self.checkpoint_manager.list_checkpoints(path)
521
521
522 def restore_checkpoint(self, checkpoint_id, path):
522 def restore_checkpoint(self, checkpoint_id, path):
523 """
523 """
524 Restore a checkpoint.
524 Restore a checkpoint.
525 """
525 """
526 type = self.get(path, content=False)['type']
526 return self.save(
527 content, format = self.checkpoint_manager.get_checkpoint(
527 model=self.checkpoint_manager.get_checkpoint(
528 checkpoint_id,
528 checkpoint_id,
529 path,
529 path,
530 type,
530 self.get(path, content=False)['type']
531 ),
532 path=path,
531 )
533 )
532
534
533 model = {
534 'type': type,
535 'content': content,
536 'format': format,
537 }
538 return self.save(model, path)
539
540 def delete_checkpoint(self, checkpoint_id, path):
535 def delete_checkpoint(self, checkpoint_id, path):
541 return self.checkpoint_manager.delete_checkpoint(checkpoint_id, path)
536 return self.checkpoint_manager.delete_checkpoint(checkpoint_id, path)
General Comments 0
You need to be logged in to leave comments. Login now