##// END OF EJS Templates
Fix contentsmanager api error message to give the path name
Jason Grout -
Show More
@@ -1,472 +1,472 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
7 7 import io
8 8 import os
9 9 import shutil
10 10 import mimetypes
11 11
12 12 from tornado import web
13 13
14 14 from .filecheckpoints import FileCheckpoints
15 15 from .fileio import FileManagerMixin
16 16 from .manager import ContentsManager
17 17
18 18 from IPython import nbformat
19 19 from IPython.utils.importstring import import_item
20 20 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
21 21 from IPython.utils.py3compat import getcwd, string_types
22 22 from IPython.utils import tz
23 23 from IPython.html.utils import (
24 24 is_hidden,
25 25 to_api_path,
26 26 )
27 27
28 28 _script_exporter = None
29 29
30 30
31 31 def _post_save_script(model, os_path, contents_manager, **kwargs):
32 32 """convert notebooks to Python script after save with nbconvert
33 33
34 34 replaces `ipython notebook --script`
35 35 """
36 36 from IPython.nbconvert.exporters.script import ScriptExporter
37 37
38 38 if model['type'] != 'notebook':
39 39 return
40 40
41 41 global _script_exporter
42 42 if _script_exporter is None:
43 43 _script_exporter = ScriptExporter(parent=contents_manager)
44 44 log = contents_manager.log
45 45
46 46 base, ext = os.path.splitext(os_path)
47 47 py_fname = base + '.py'
48 48 script, resources = _script_exporter.from_filename(os_path)
49 49 script_fname = base + resources.get('output_extension', '.txt')
50 50 log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
51 51 with io.open(script_fname, 'w', encoding='utf-8') as f:
52 52 f.write(script)
53 53
54 54
55 55 class FileContentsManager(FileManagerMixin, ContentsManager):
56 56
57 57 root_dir = Unicode(config=True)
58 58
59 59 def _root_dir_default(self):
60 60 try:
61 61 return self.parent.notebook_dir
62 62 except AttributeError:
63 63 return getcwd()
64 64
65 65 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
66 66 def _save_script_changed(self):
67 67 self.log.warn("""
68 68 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
69 69
70 70 ContentsManager.pre_save_hook
71 71 FileContentsManager.post_save_hook
72 72
73 73 A post-save hook has been registered that calls:
74 74
75 75 ipython nbconvert --to script [notebook]
76 76
77 77 which behaves similarly to `--script`.
78 78 """)
79 79
80 80 self.post_save_hook = _post_save_script
81 81
82 82 post_save_hook = Any(None, config=True,
83 83 help="""Python callable or importstring thereof
84 84
85 85 to be called on the path of a file just saved.
86 86
87 87 This can be used to process the file on disk,
88 88 such as converting the notebook to a script or HTML via nbconvert.
89 89
90 90 It will be called as (all arguments passed by keyword):
91 91
92 92 hook(os_path=os_path, model=model, contents_manager=instance)
93 93
94 94 path: the filesystem path to the file just written
95 95 model: the model representing the file
96 96 contents_manager: this ContentsManager instance
97 97 """
98 98 )
99 99 def _post_save_hook_changed(self, name, old, new):
100 100 if new and isinstance(new, string_types):
101 101 self.post_save_hook = import_item(self.post_save_hook)
102 102 elif new:
103 103 if not callable(new):
104 104 raise TraitError("post_save_hook must be callable")
105 105
106 106 def run_post_save_hook(self, model, os_path):
107 107 """Run the post-save hook if defined, and log errors"""
108 108 if self.post_save_hook:
109 109 try:
110 110 self.log.debug("Running post-save hook on %s", os_path)
111 111 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
112 112 except Exception:
113 113 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
114 114
115 115 def _root_dir_changed(self, name, old, new):
116 116 """Do a bit of validation of the root_dir."""
117 117 if not os.path.isabs(new):
118 118 # If we receive a non-absolute path, make it absolute.
119 119 self.root_dir = os.path.abspath(new)
120 120 return
121 121 if not os.path.isdir(new):
122 122 raise TraitError("%r is not a directory" % new)
123 123
124 124 def _checkpoints_class_default(self):
125 125 return FileCheckpoints
126 126
127 127 def is_hidden(self, path):
128 128 """Does the API style path correspond to a hidden directory or file?
129 129
130 130 Parameters
131 131 ----------
132 132 path : string
133 133 The path to check. This is an API path (`/` separated,
134 134 relative to root_dir).
135 135
136 136 Returns
137 137 -------
138 138 hidden : bool
139 139 Whether the path exists and is hidden.
140 140 """
141 141 path = path.strip('/')
142 142 os_path = self._get_os_path(path=path)
143 143 return is_hidden(os_path, self.root_dir)
144 144
145 145 def file_exists(self, path):
146 146 """Returns True if the file exists, else returns False.
147 147
148 148 API-style wrapper for os.path.isfile
149 149
150 150 Parameters
151 151 ----------
152 152 path : string
153 153 The relative path to the file (with '/' as separator)
154 154
155 155 Returns
156 156 -------
157 157 exists : bool
158 158 Whether the file exists.
159 159 """
160 160 path = path.strip('/')
161 161 os_path = self._get_os_path(path)
162 162 return os.path.isfile(os_path)
163 163
164 164 def dir_exists(self, path):
165 165 """Does the API-style path refer to an extant directory?
166 166
167 167 API-style wrapper for os.path.isdir
168 168
169 169 Parameters
170 170 ----------
171 171 path : string
172 172 The path to check. This is an API path (`/` separated,
173 173 relative to root_dir).
174 174
175 175 Returns
176 176 -------
177 177 exists : bool
178 178 Whether the path is indeed a directory.
179 179 """
180 180 path = path.strip('/')
181 181 os_path = self._get_os_path(path=path)
182 182 return os.path.isdir(os_path)
183 183
184 184 def exists(self, path):
185 185 """Returns True if the path exists, else returns False.
186 186
187 187 API-style wrapper for os.path.exists
188 188
189 189 Parameters
190 190 ----------
191 191 path : string
192 192 The API path to the file (with '/' as separator)
193 193
194 194 Returns
195 195 -------
196 196 exists : bool
197 197 Whether the target exists.
198 198 """
199 199 path = path.strip('/')
200 200 os_path = self._get_os_path(path=path)
201 201 return os.path.exists(os_path)
202 202
203 203 def _base_model(self, path):
204 204 """Build the common base of a contents model"""
205 205 os_path = self._get_os_path(path)
206 206 info = os.stat(os_path)
207 207 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 208 created = tz.utcfromtimestamp(info.st_ctime)
209 209 # Create the base model.
210 210 model = {}
211 211 model['name'] = path.rsplit('/', 1)[-1]
212 212 model['path'] = path
213 213 model['last_modified'] = last_modified
214 214 model['created'] = created
215 215 model['content'] = None
216 216 model['format'] = None
217 217 model['mimetype'] = None
218 218 try:
219 219 model['writable'] = os.access(os_path, os.W_OK)
220 220 except OSError:
221 221 self.log.error("Failed to check write permissions on %s", os_path)
222 222 model['writable'] = False
223 223 return model
224 224
225 225 def _dir_model(self, path, content=True):
226 226 """Build a model for a directory
227 227
228 228 if content is requested, will include a listing of the directory
229 229 """
230 230 os_path = self._get_os_path(path)
231 231
232 232 four_o_four = u'directory does not exist: %r' % path
233 233
234 234 if not os.path.isdir(os_path):
235 235 raise web.HTTPError(404, four_o_four)
236 236 elif is_hidden(os_path, self.root_dir):
237 237 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
238 238 os_path
239 239 )
240 240 raise web.HTTPError(404, four_o_four)
241 241
242 242 model = self._base_model(path)
243 243 model['type'] = 'directory'
244 244 if content:
245 245 model['content'] = contents = []
246 246 os_dir = self._get_os_path(path)
247 247 for name in os.listdir(os_dir):
248 248 os_path = os.path.join(os_dir, name)
249 249 # skip over broken symlinks in listing
250 250 if not os.path.exists(os_path):
251 251 self.log.warn("%s doesn't exist", os_path)
252 252 continue
253 253 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
254 254 self.log.debug("%s not a regular file", os_path)
255 255 continue
256 256 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
257 257 contents.append(self.get(
258 258 path='%s/%s' % (path, name),
259 259 content=False)
260 260 )
261 261
262 262 model['format'] = 'json'
263 263
264 264 return model
265 265
266 266 def _file_model(self, path, content=True, format=None):
267 267 """Build a model for a file
268 268
269 269 if content is requested, include the file contents.
270 270
271 271 format:
272 272 If 'text', the contents will be decoded as UTF-8.
273 273 If 'base64', the raw bytes contents will be encoded as base64.
274 274 If not specified, try to decode as UTF-8, and fall back to base64
275 275 """
276 276 model = self._base_model(path)
277 277 model['type'] = 'file'
278 278
279 279 os_path = self._get_os_path(path)
280 280
281 281 if content:
282 282 content, format = self._read_file(os_path, format)
283 283 default_mime = {
284 284 'text': 'text/plain',
285 285 'base64': 'application/octet-stream'
286 286 }[format]
287 287
288 288 model.update(
289 289 content=content,
290 290 format=format,
291 291 mimetype=mimetypes.guess_type(os_path)[0] or default_mime,
292 292 )
293 293
294 294 return model
295 295
296 296 def _notebook_model(self, path, content=True):
297 297 """Build a notebook model
298 298
299 299 if content is requested, the notebook content will be populated
300 300 as a JSON structure (not double-serialized)
301 301 """
302 302 model = self._base_model(path)
303 303 model['type'] = 'notebook'
304 304 if content:
305 305 os_path = self._get_os_path(path)
306 306 nb = self._read_notebook(os_path, as_version=4)
307 307 self.mark_trusted_cells(nb, path)
308 308 model['content'] = nb
309 309 model['format'] = 'json'
310 310 self.validate_notebook_model(model)
311 311 return model
312 312
313 313 def get(self, path, content=True, type=None, format=None):
314 314 """ Takes a path for an entity and returns its model
315 315
316 316 Parameters
317 317 ----------
318 318 path : str
319 319 the API path that describes the relative path for the target
320 320 content : bool
321 321 Whether to include the contents in the reply
322 322 type : str, optional
323 323 The requested type - 'file', 'notebook', or 'directory'.
324 324 Will raise HTTPError 400 if the content doesn't match.
325 325 format : str, optional
326 326 The requested format for file contents. 'text' or 'base64'.
327 327 Ignored if this returns a notebook or directory model.
328 328
329 329 Returns
330 330 -------
331 331 model : dict
332 332 the contents model. If content=True, returns the contents
333 333 of the file or directory as well.
334 334 """
335 335 path = path.strip('/')
336 336
337 337 if not self.exists(path):
338 338 raise web.HTTPError(404, u'No such file or directory: %s' % path)
339 339
340 340 os_path = self._get_os_path(path)
341 341 if os.path.isdir(os_path):
342 342 if type not in (None, 'directory'):
343 343 raise web.HTTPError(400,
344 344 u'%s is a directory, not a %s' % (path, type), reason='bad type')
345 345 model = self._dir_model(path, content=content)
346 346 elif type == 'notebook' or (type is None and path.endswith('.ipynb')):
347 347 model = self._notebook_model(path, content=content)
348 348 else:
349 349 if type == 'directory':
350 350 raise web.HTTPError(400,
351 u'%s is not a directory', reason='bad type')
351 u'%s is not a directory' % path, reason='bad type')
352 352 model = self._file_model(path, content=content, format=format)
353 353 return model
354 354
355 355 def _save_directory(self, os_path, model, path=''):
356 356 """create a directory"""
357 357 if is_hidden(os_path, self.root_dir):
358 358 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
359 359 if not os.path.exists(os_path):
360 360 with self.perm_to_403():
361 361 os.mkdir(os_path)
362 362 elif not os.path.isdir(os_path):
363 363 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
364 364 else:
365 365 self.log.debug("Directory %r already exists", os_path)
366 366
367 367 def save(self, model, path=''):
368 368 """Save the file model and return the model with no content."""
369 369 path = path.strip('/')
370 370
371 371 if 'type' not in model:
372 372 raise web.HTTPError(400, u'No file type provided')
373 373 if 'content' not in model and model['type'] != 'directory':
374 374 raise web.HTTPError(400, u'No file content provided')
375 375
376 376 self.run_pre_save_hook(model=model, path=path)
377 377
378 378 os_path = self._get_os_path(path)
379 379 self.log.debug("Saving %s", os_path)
380 380 try:
381 381 if model['type'] == 'notebook':
382 382 nb = nbformat.from_dict(model['content'])
383 383 self.check_and_sign(nb, path)
384 384 self._save_notebook(os_path, nb)
385 385 # One checkpoint should always exist for notebooks.
386 386 if not self.checkpoints.list_checkpoints(path):
387 387 self.create_checkpoint(path)
388 388 elif model['type'] == 'file':
389 389 # Missing format will be handled internally by _save_file.
390 390 self._save_file(os_path, model['content'], model.get('format'))
391 391 elif model['type'] == 'directory':
392 392 self._save_directory(os_path, model, path)
393 393 else:
394 394 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
395 395 except web.HTTPError:
396 396 raise
397 397 except Exception as e:
398 398 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
399 399 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
400 400
401 401 validation_message = None
402 402 if model['type'] == 'notebook':
403 403 self.validate_notebook_model(model)
404 404 validation_message = model.get('message', None)
405 405
406 406 model = self.get(path, content=False)
407 407 if validation_message:
408 408 model['message'] = validation_message
409 409
410 410 self.run_post_save_hook(model=model, os_path=os_path)
411 411
412 412 return model
413 413
414 414 def delete_file(self, path):
415 415 """Delete file at path."""
416 416 path = path.strip('/')
417 417 os_path = self._get_os_path(path)
418 418 rm = os.unlink
419 419 if os.path.isdir(os_path):
420 420 listing = os.listdir(os_path)
421 421 # Don't delete non-empty directories.
422 422 # A directory containing only leftover checkpoints is
423 423 # considered empty.
424 424 cp_dir = getattr(self.checkpoints, 'checkpoint_dir', None)
425 425 for entry in listing:
426 426 if entry != cp_dir:
427 427 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
428 428 elif not os.path.isfile(os_path):
429 429 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
430 430
431 431 if os.path.isdir(os_path):
432 432 self.log.debug("Removing directory %s", os_path)
433 433 with self.perm_to_403():
434 434 shutil.rmtree(os_path)
435 435 else:
436 436 self.log.debug("Unlinking file %s", os_path)
437 437 with self.perm_to_403():
438 438 rm(os_path)
439 439
440 440 def rename_file(self, old_path, new_path):
441 441 """Rename a file."""
442 442 old_path = old_path.strip('/')
443 443 new_path = new_path.strip('/')
444 444 if new_path == old_path:
445 445 return
446 446
447 447 new_os_path = self._get_os_path(new_path)
448 448 old_os_path = self._get_os_path(old_path)
449 449
450 450 # Should we proceed with the move?
451 451 if os.path.exists(new_os_path):
452 452 raise web.HTTPError(409, u'File already exists: %s' % new_path)
453 453
454 454 # Move the file
455 455 try:
456 456 with self.perm_to_403():
457 457 shutil.move(old_os_path, new_os_path)
458 458 except web.HTTPError:
459 459 raise
460 460 except Exception as e:
461 461 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
462 462
463 463 def info_string(self):
464 464 return "Serving notebooks from local directory: %s" % self.root_dir
465 465
466 466 def get_kernel_path(self, path, model=None):
467 467 """Return the initial API path of a kernel associated with a given notebook"""
468 468 if '/' in path:
469 469 parent_dir = path.rsplit('/', 1)[0]
470 470 else:
471 471 parent_dir = ''
472 472 return parent_dir
General Comments 0
You need to be logged in to leave comments. Login now