##// END OF EJS Templates
Fix various review comments
Thomas Kluyver -
Show More
@@ -1,559 +1,559 b''
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import base64
7 7 import io
8 8 import os
9 9 import glob
10 10 import shutil
11 11
12 12 from tornado import web
13 13
14 14 from .manager import ContentsManager
15 15 from IPython import nbformat
16 16 from IPython.utils.io import atomic_writing
17 17 from IPython.utils.path import ensure_dir_exists
18 18 from IPython.utils.traitlets import Unicode, Bool, TraitError
19 19 from IPython.utils.py3compat import getcwd
20 20 from IPython.utils import tz
21 21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
22 22
23 23
24 24 class FileContentsManager(ContentsManager):
25 25
26 26 root_dir = Unicode(getcwd(), config=True)
27 27
28 28 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
29 29 def _save_script_changed(self):
30 30 self.log.warn("""
31 31 Automatically saving notebooks as scripts has been removed.
32 32 Use `ipython nbconvert --to python [notebook]` instead.
33 33 """)
34 34
35 35 def _root_dir_changed(self, name, old, new):
36 36 """Do a bit of validation of the root_dir."""
37 37 if not os.path.isabs(new):
38 38 # If we receive a non-absolute path, make it absolute.
39 39 self.root_dir = os.path.abspath(new)
40 40 return
41 41 if not os.path.isdir(new):
42 42 raise TraitError("%r is not a directory" % new)
43 43
44 44 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
45 45 help="""The directory name in which to keep file checkpoints
46 46
47 47 This is a path relative to the file's own directory.
48 48
49 49 By default, it is .ipynb_checkpoints
50 50 """
51 51 )
52 52
53 53 def _copy(self, src, dest):
54 54 """copy src to dest
55 55
56 56 like shutil.copy2, but log errors in copystat
57 57 """
58 58 shutil.copyfile(src, dest)
59 59 try:
60 60 shutil.copystat(src, dest)
61 61 except OSError as e:
62 62 self.log.debug("copystat on %s failed", dest, exc_info=True)
63 63
64 64 def _get_os_path(self, path):
65 65 """Given an API path, return its file system path.
66 66
67 67 Parameters
68 68 ----------
69 69 path : string
70 70 The relative API path to the named file.
71 71
72 72 Returns
73 73 -------
74 74 path : string
75 75 Native, absolute OS path to for a file.
76 76 """
77 77 return to_os_path(path, self.root_dir)
78 78
79 79 def dir_exists(self, path):
80 80 """Does the API-style path refer to an extant directory?
81 81
82 82 API-style wrapper for os.path.isdir
83 83
84 84 Parameters
85 85 ----------
86 86 path : string
87 87 The path to check. This is an API path (`/` separated,
88 88 relative to root_dir).
89 89
90 90 Returns
91 91 -------
92 92 exists : bool
93 93 Whether the path is indeed a directory.
94 94 """
95 95 path = path.strip('/')
96 96 os_path = self._get_os_path(path=path)
97 97 return os.path.isdir(os_path)
98 98
99 99 def is_hidden(self, path):
100 100 """Does the API style path correspond to a hidden directory or file?
101 101
102 102 Parameters
103 103 ----------
104 104 path : string
105 105 The path to check. This is an API path (`/` separated,
106 106 relative to root_dir).
107 107
108 108 Returns
109 109 -------
110 110 hidden : bool
111 111 Whether the path exists and is hidden.
112 112 """
113 113 path = path.strip('/')
114 114 os_path = self._get_os_path(path=path)
115 115 return is_hidden(os_path, self.root_dir)
116 116
117 117 def file_exists(self, path):
118 118 """Returns True if the file exists, else returns False.
119 119
120 120 API-style wrapper for os.path.isfile
121 121
122 122 Parameters
123 123 ----------
124 124 path : string
125 125 The relative path to the file (with '/' as separator)
126 126
127 127 Returns
128 128 -------
129 129 exists : bool
130 130 Whether the file exists.
131 131 """
132 132 path = path.strip('/')
133 133 os_path = self._get_os_path(path)
134 134 return os.path.isfile(os_path)
135 135
136 136 def exists(self, path):
137 137 """Returns True if the path exists, else returns False.
138 138
139 139 API-style wrapper for os.path.exists
140 140
141 141 Parameters
142 142 ----------
143 143 path : string
144 144 The API path to the file (with '/' as separator)
145 145
146 146 Returns
147 147 -------
148 148 exists : bool
149 149 Whether the target exists.
150 150 """
151 151 path = path.strip('/')
152 152 os_path = self._get_os_path(path=path)
153 153 return os.path.exists(os_path)
154 154
155 155 def _base_model(self, path):
156 156 """Build the common base of a contents model"""
157 157 os_path = self._get_os_path(path)
158 158 info = os.stat(os_path)
159 159 last_modified = tz.utcfromtimestamp(info.st_mtime)
160 160 created = tz.utcfromtimestamp(info.st_ctime)
161 161 # Create the base model.
162 162 model = {}
163 163 model['name'] = path.rsplit('/', 1)[-1]
164 164 model['path'] = path
165 165 model['last_modified'] = last_modified
166 166 model['created'] = created
167 167 model['content'] = None
168 168 model['format'] = None
169 169 return model
170 170
171 171 def _dir_model(self, path, content=True):
172 172 """Build a model for a directory
173 173
174 174 if content is requested, will include a listing of the directory
175 175 """
176 176 os_path = self._get_os_path(path)
177 177
178 178 four_o_four = u'directory does not exist: %r' % os_path
179 179
180 180 if not os.path.isdir(os_path):
181 181 raise web.HTTPError(404, four_o_four)
182 182 elif is_hidden(os_path, self.root_dir):
183 183 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
184 184 os_path
185 185 )
186 186 raise web.HTTPError(404, four_o_four)
187 187
188 188 model = self._base_model(path)
189 189 model['type'] = 'directory'
190 190 if content:
191 191 model['content'] = contents = []
192 192 os_dir = self._get_os_path(path)
193 193 for name in os.listdir(os_dir):
194 194 os_path = os.path.join(os_dir, name)
195 195 # skip over broken symlinks in listing
196 196 if not os.path.exists(os_path):
197 197 self.log.warn("%s doesn't exist", os_path)
198 198 continue
199 199 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
200 200 self.log.debug("%s not a regular file", os_path)
201 201 continue
202 202 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
203 203 contents.append(self.get_model(
204 204 path='%s/%s' % (path, name),
205 205 content=False)
206 206 )
207 207
208 208 model['format'] = 'json'
209 209
210 210 return model
211 211
212 212 def _file_model(self, path, content=True, format=None):
213 213 """Build a model for a file
214 214
215 215 if content is requested, include the file contents.
216 216
217 217 format:
218 218 If 'text', the contents will be decoded as UTF-8.
219 219 If 'base64', the raw bytes contents will be encoded as base64.
220 220 If not specified, try to decode as UTF-8, and fall back to base64
221 221 """
222 222 model = self._base_model(path)
223 223 model['type'] = 'file'
224 224 if content:
225 225 os_path = self._get_os_path(path)
226 226 if not os.path.isfile(os_path):
227 227 # could be FIFO
228 228 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
229 229 with io.open(os_path, 'rb') as f:
230 230 bcontent = f.read()
231 231
232 232 if format != 'base64':
233 233 try:
234 234 model['content'] = bcontent.decode('utf8')
235 235 except UnicodeError as e:
236 236 if format == 'text':
237 237 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
238 238 else:
239 239 model['format'] = 'text'
240 240
241 241 if model['content'] is None:
242 242 model['content'] = base64.encodestring(bcontent).decode('ascii')
243 243 model['format'] = 'base64'
244 244
245 245 return model
246 246
247 247
248 248 def _notebook_model(self, path, content=True):
249 249 """Build a notebook model
250 250
251 251 if content is requested, the notebook content will be populated
252 252 as a JSON structure (not double-serialized)
253 253 """
254 254 model = self._base_model(path)
255 255 model['type'] = 'notebook'
256 256 if content:
257 257 os_path = self._get_os_path(path)
258 258 with io.open(os_path, 'r', encoding='utf-8') as f:
259 259 try:
260 260 nb = nbformat.read(f, as_version=4)
261 261 except Exception as e:
262 262 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
263 263 self.mark_trusted_cells(nb, path)
264 264 model['content'] = nb
265 265 model['format'] = 'json'
266 266 self.validate_notebook_model(model)
267 267 return model
268 268
269 269 def get_model(self, path, content=True, type_=None, format=None):
270 270 """ Takes a path for an entity and returns its model
271 271
272 272 Parameters
273 273 ----------
274 274 path : str
275 275 the API path that describes the relative path for the target
276 276 content : bool
277 277 Whether to include the contents in the reply
278 278 type_ : str, optional
279 279 The requested type - 'file', 'notebook', or 'directory'.
280 Will raise HTTPError 406 if the content doesn't match.
280 Will raise HTTPError 400 if the content doesn't match.
281 281 format : str, optional
282 282 The requested format for file contents. 'text' or 'base64'.
283 283 Ignored if this returns a notebook or directory model.
284 284
285 285 Returns
286 286 -------
287 287 model : dict
288 288 the contents model. If content=True, returns the contents
289 289 of the file or directory as well.
290 290 """
291 291 path = path.strip('/')
292 292
293 293 if not self.exists(path):
294 294 raise web.HTTPError(404, u'No such file or directory: %s' % path)
295 295
296 296 os_path = self._get_os_path(path)
297 297 if os.path.isdir(os_path):
298 298 if type_ not in (None, 'directory'):
299 299 raise web.HTTPError(400,
300 300 u'%s is a directory, not a %s' % (path, type_))
301 301 model = self._dir_model(path, content=content)
302 302 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
303 303 model = self._notebook_model(path, content=content)
304 304 else:
305 305 if type_ == 'directory':
306 306 raise web.HTTPError(400,
307 307 u'%s is not a directory')
308 308 model = self._file_model(path, content=content, format=format)
309 309 return model
310 310
311 311 def _save_notebook(self, os_path, model, path=''):
312 312 """save a notebook file"""
313 313 # Save the notebook file
314 314 nb = nbformat.from_dict(model['content'])
315 315
316 316 self.check_and_sign(nb, path)
317 317
318 318 with atomic_writing(os_path, encoding='utf-8') as f:
319 319 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
320 320
321 321 def _save_file(self, os_path, model, path=''):
322 322 """save a non-notebook file"""
323 323 fmt = model.get('format', None)
324 324 if fmt not in {'text', 'base64'}:
325 325 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
326 326 try:
327 327 content = model['content']
328 328 if fmt == 'text':
329 329 bcontent = content.encode('utf8')
330 330 else:
331 331 b64_bytes = content.encode('ascii')
332 332 bcontent = base64.decodestring(b64_bytes)
333 333 except Exception as e:
334 334 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
335 335 with atomic_writing(os_path, text=False) as f:
336 336 f.write(bcontent)
337 337
338 338 def _save_directory(self, os_path, model, path=''):
339 339 """create a directory"""
340 340 if is_hidden(os_path, self.root_dir):
341 341 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
342 342 if not os.path.exists(os_path):
343 343 os.mkdir(os_path)
344 344 elif not os.path.isdir(os_path):
345 345 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
346 346 else:
347 347 self.log.debug("Directory %r already exists", os_path)
348 348
349 349 def save(self, model, path=''):
350 350 """Save the file model and return the model with no content."""
351 351 path = path.strip('/')
352 352
353 353 if 'type' not in model:
354 354 raise web.HTTPError(400, u'No file type provided')
355 355 if 'content' not in model and model['type'] != 'directory':
356 356 raise web.HTTPError(400, u'No file content provided')
357 357
358 358 # One checkpoint should always exist
359 359 if self.file_exists(path) and not self.list_checkpoints(path):
360 360 self.create_checkpoint(path)
361 361
362 362 os_path = self._get_os_path(path)
363 363 self.log.debug("Saving %s", os_path)
364 364 try:
365 365 if model['type'] == 'notebook':
366 366 self._save_notebook(os_path, model, path)
367 367 elif model['type'] == 'file':
368 368 self._save_file(os_path, model, path)
369 369 elif model['type'] == 'directory':
370 370 self._save_directory(os_path, model, path)
371 371 else:
372 372 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
373 373 except web.HTTPError:
374 374 raise
375 375 except Exception as e:
376 376 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
377 377
378 378 validation_message = None
379 379 if model['type'] == 'notebook':
380 380 self.validate_notebook_model(model)
381 381 validation_message = model.get('message', None)
382 382
383 383 model = self.get_model(path, content=False)
384 384 if validation_message:
385 385 model['message'] = validation_message
386 386 return model
387 387
388 388 def update(self, model, path):
389 389 """Update the file's path
390 390
391 391 For use in PATCH requests, to enable renaming a file without
392 392 re-uploading its contents. Only used for renaming at the moment.
393 393 """
394 394 path = path.strip('/')
395 395 new_path = model.get('path', path).strip('/')
396 396 if path != new_path:
397 397 self.rename(path, new_path)
398 398 model = self.get_model(new_path, content=False)
399 399 return model
400 400
401 401 def delete(self, path):
402 402 """Delete file at path."""
403 403 path = path.strip('/')
404 404 os_path = self._get_os_path(path)
405 405 rm = os.unlink
406 406 if os.path.isdir(os_path):
407 407 listing = os.listdir(os_path)
408 408 # don't delete non-empty directories (checkpoints dir doesn't count)
409 409 if listing and listing != [self.checkpoint_dir]:
410 410 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
411 411 elif not os.path.isfile(os_path):
412 412 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
413 413
414 414 # clear checkpoints
415 415 for checkpoint in self.list_checkpoints(path):
416 416 checkpoint_id = checkpoint['id']
417 417 cp_path = self.get_checkpoint_path(checkpoint_id, path)
418 418 if os.path.isfile(cp_path):
419 419 self.log.debug("Unlinking checkpoint %s", cp_path)
420 420 os.unlink(cp_path)
421 421
422 422 if os.path.isdir(os_path):
423 423 self.log.debug("Removing directory %s", os_path)
424 424 shutil.rmtree(os_path)
425 425 else:
426 426 self.log.debug("Unlinking file %s", os_path)
427 427 rm(os_path)
428 428
429 429 def rename(self, old_path, new_path):
430 430 """Rename a file."""
431 431 old_path = old_path.strip('/')
432 432 new_path = new_path.strip('/')
433 433 if new_path == old_path:
434 434 return
435 435
436 436 new_os_path = self._get_os_path(new_path)
437 437 old_os_path = self._get_os_path(old_path)
438 438
439 439 # Should we proceed with the move?
440 440 if os.path.exists(new_os_path):
441 441 raise web.HTTPError(409, u'File already exists: %s' % new_path)
442 442
443 443 # Move the file
444 444 try:
445 445 shutil.move(old_os_path, new_os_path)
446 446 except Exception as e:
447 447 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
448 448
449 449 # Move the checkpoints
450 450 old_checkpoints = self.list_checkpoints(old_path)
451 451 for cp in old_checkpoints:
452 452 checkpoint_id = cp['id']
453 453 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
454 454 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
455 455 if os.path.isfile(old_cp_path):
456 456 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
457 457 shutil.move(old_cp_path, new_cp_path)
458 458
459 459 # Checkpoint-related utilities
460 460
461 461 def get_checkpoint_path(self, checkpoint_id, path):
462 462 """find the path to a checkpoint"""
463 463 path = path.strip('/')
464 464 parent, name = ('/' + path).rsplit('/', 1)
465 465 parent = parent.strip('/')
466 466 basename, ext = os.path.splitext(name)
467 467 filename = u"{name}-{checkpoint_id}{ext}".format(
468 468 name=basename,
469 469 checkpoint_id=checkpoint_id,
470 470 ext=ext,
471 471 )
472 472 os_path = self._get_os_path(path=parent)
473 473 cp_dir = os.path.join(os_path, self.checkpoint_dir)
474 474 ensure_dir_exists(cp_dir)
475 475 cp_path = os.path.join(cp_dir, filename)
476 476 return cp_path
477 477
478 478 def get_checkpoint_model(self, checkpoint_id, path):
479 479 """construct the info dict for a given checkpoint"""
480 480 path = path.strip('/')
481 481 cp_path = self.get_checkpoint_path(checkpoint_id, path)
482 482 stats = os.stat(cp_path)
483 483 last_modified = tz.utcfromtimestamp(stats.st_mtime)
484 484 info = dict(
485 485 id = checkpoint_id,
486 486 last_modified = last_modified,
487 487 )
488 488 return info
489 489
490 490 # public checkpoint API
491 491
492 492 def create_checkpoint(self, path):
493 493 """Create a checkpoint from the current state of a file"""
494 494 path = path.strip('/')
495 495 if not self.file_exists(path):
496 496 raise web.HTTPError(404)
497 497 src_path = self._get_os_path(path)
498 498 # only the one checkpoint ID:
499 499 checkpoint_id = u"checkpoint"
500 500 cp_path = self.get_checkpoint_path(checkpoint_id, path)
501 501 self.log.debug("creating checkpoint for %s", path)
502 502 self._copy(src_path, cp_path)
503 503
504 504 # return the checkpoint info
505 505 return self.get_checkpoint_model(checkpoint_id, path)
506 506
507 507 def list_checkpoints(self, path):
508 508 """list the checkpoints for a given file
509 509
510 510 This contents manager currently only supports one checkpoint per file.
511 511 """
512 512 path = path.strip('/')
513 513 checkpoint_id = "checkpoint"
514 514 os_path = self.get_checkpoint_path(checkpoint_id, path)
515 515 if not os.path.exists(os_path):
516 516 return []
517 517 else:
518 518 return [self.get_checkpoint_model(checkpoint_id, path)]
519 519
520 520
521 521 def restore_checkpoint(self, checkpoint_id, path):
522 522 """restore a file to a checkpointed state"""
523 523 path = path.strip('/')
524 524 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
525 525 nb_path = self._get_os_path(path)
526 526 cp_path = self.get_checkpoint_path(checkpoint_id, path)
527 527 if not os.path.isfile(cp_path):
528 528 self.log.debug("checkpoint file does not exist: %s", cp_path)
529 529 raise web.HTTPError(404,
530 530 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
531 531 )
532 532 # ensure notebook is readable (never restore from an unreadable notebook)
533 533 if cp_path.endswith('.ipynb'):
534 534 with io.open(cp_path, 'r', encoding='utf-8') as f:
535 535 nbformat.read(f, as_version=4)
536 536 self._copy(cp_path, nb_path)
537 537 self.log.debug("copying %s -> %s", cp_path, nb_path)
538 538
539 539 def delete_checkpoint(self, checkpoint_id, path):
540 540 """delete a file's checkpoint"""
541 541 path = path.strip('/')
542 542 cp_path = self.get_checkpoint_path(checkpoint_id, path)
543 543 if not os.path.isfile(cp_path):
544 544 raise web.HTTPError(404,
545 545 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
546 546 )
547 547 self.log.debug("unlinking %s", cp_path)
548 548 os.unlink(cp_path)
549 549
550 550 def info_string(self):
551 551 return "Serving notebooks from local directory: %s" % self.root_dir
552 552
553 553 def get_kernel_path(self, path, model=None):
554 554 """Return the initial working dir a kernel associated with a given notebook"""
555 555 if '/' in path:
556 556 parent_dir = path.rsplit('/', 1)[0]
557 557 else:
558 558 parent_dir = ''
559 559 return self._get_os_path(parent_dir)
@@ -1,513 +1,511 b''
1 1 # coding: utf-8
2 2 """Test the contents webservice API."""
3 3
4 4 import base64
5 5 import io
6 6 import json
7 7 import os
8 8 import shutil
9 9 from unicodedata import normalize
10 10
11 11 pjoin = os.path.join
12 12
13 13 import requests
14 14
15 15 from IPython.html.utils import url_path_join, url_escape
16 16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
17 17 from IPython.nbformat import read, write, from_dict
18 18 from IPython.nbformat.v4 import (
19 19 new_notebook, new_markdown_cell,
20 20 )
21 21 from IPython.nbformat import v2
22 22 from IPython.utils import py3compat
23 23 from IPython.utils.data import uniq_stable
24 24
25 25
26 26 def notebooks_only(dir_model):
27 27 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
28 28
29 29 def dirs_only(dir_model):
30 30 return [x for x in dir_model['content'] if x['type']=='directory']
31 31
32 32
33 33 class API(object):
34 34 """Wrapper for contents API calls."""
35 35 def __init__(self, base_url):
36 36 self.base_url = base_url
37 37
38 def _req(self, verb, path, body=None):
38 def _req(self, verb, path, body=None, params=None):
39 39 response = requests.request(verb,
40 40 url_path_join(self.base_url, 'api/contents', path),
41 41 data=body,
42 42 )
43 43 response.raise_for_status()
44 44 return response
45 45
46 46 def list(self, path='/'):
47 47 return self._req('GET', path)
48 48
49 49 def read(self, path, type_=None, format=None):
50 query = []
50 params = {}
51 51 if type_ is not None:
52 query.append('type=' + type_)
52 params['type'] = type_
53 53 if format is not None:
54 query.append('format=' + format)
55 if query:
56 path += '?' + '&'.join(query)
57 return self._req('GET', path)
54 params['format'] = format
55 return self._req('GET', path, params=params)
58 56
59 57 def create_untitled(self, path='/', ext='.ipynb'):
60 58 body = None
61 59 if ext:
62 60 body = json.dumps({'ext': ext})
63 61 return self._req('POST', path, body)
64 62
65 63 def mkdir_untitled(self, path='/'):
66 64 return self._req('POST', path, json.dumps({'type': 'directory'}))
67 65
68 66 def copy(self, copy_from, path='/'):
69 67 body = json.dumps({'copy_from':copy_from})
70 68 return self._req('POST', path, body)
71 69
72 70 def create(self, path='/'):
73 71 return self._req('PUT', path)
74 72
75 73 def upload(self, path, body):
76 74 return self._req('PUT', path, body)
77 75
78 76 def mkdir_untitled(self, path='/'):
79 77 return self._req('POST', path, json.dumps({'type': 'directory'}))
80 78
81 79 def mkdir(self, path='/'):
82 80 return self._req('PUT', path, json.dumps({'type': 'directory'}))
83 81
84 82 def copy_put(self, copy_from, path='/'):
85 83 body = json.dumps({'copy_from':copy_from})
86 84 return self._req('PUT', path, body)
87 85
88 86 def save(self, path, body):
89 87 return self._req('PUT', path, body)
90 88
91 89 def delete(self, path='/'):
92 90 return self._req('DELETE', path)
93 91
94 92 def rename(self, path, new_path):
95 93 body = json.dumps({'path': new_path})
96 94 return self._req('PATCH', path, body)
97 95
98 96 def get_checkpoints(self, path):
99 97 return self._req('GET', url_path_join(path, 'checkpoints'))
100 98
101 99 def new_checkpoint(self, path):
102 100 return self._req('POST', url_path_join(path, 'checkpoints'))
103 101
104 102 def restore_checkpoint(self, path, checkpoint_id):
105 103 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
106 104
107 105 def delete_checkpoint(self, path, checkpoint_id):
108 106 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
109 107
110 108 class APITest(NotebookTestBase):
111 109 """Test the kernels web service API"""
112 110 dirs_nbs = [('', 'inroot'),
113 111 ('Directory with spaces in', 'inspace'),
114 112 (u'unicodΓ©', 'innonascii'),
115 113 ('foo', 'a'),
116 114 ('foo', 'b'),
117 115 ('foo', 'name with spaces'),
118 116 ('foo', u'unicodΓ©'),
119 117 ('foo/bar', 'baz'),
120 118 ('ordering', 'A'),
121 119 ('ordering', 'b'),
122 120 ('ordering', 'C'),
123 121 (u'Γ₯ b', u'Γ§ d'),
124 122 ]
125 123 hidden_dirs = ['.hidden', '__pycache__']
126 124
127 125 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
128 126 del dirs[0] # remove ''
129 127 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
130 128
131 129 @staticmethod
132 130 def _blob_for_name(name):
133 131 return name.encode('utf-8') + b'\xFF'
134 132
135 133 @staticmethod
136 134 def _txt_for_name(name):
137 135 return u'%s text file' % name
138 136
139 137 def setUp(self):
140 138 nbdir = self.notebook_dir.name
141 139 self.blob = os.urandom(100)
142 140 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
143 141
144 142 for d in (self.dirs + self.hidden_dirs):
145 143 d.replace('/', os.sep)
146 144 if not os.path.isdir(pjoin(nbdir, d)):
147 145 os.mkdir(pjoin(nbdir, d))
148 146
149 147 for d, name in self.dirs_nbs:
150 148 d = d.replace('/', os.sep)
151 149 # create a notebook
152 150 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
153 151 encoding='utf-8') as f:
154 152 nb = new_notebook()
155 153 write(nb, f, version=4)
156 154
157 155 # create a text file
158 156 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
159 157 encoding='utf-8') as f:
160 158 f.write(self._txt_for_name(name))
161 159
162 160 # create a binary file
163 161 with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f:
164 162 f.write(self._blob_for_name(name))
165 163
166 164 self.api = API(self.base_url())
167 165
168 166 def tearDown(self):
169 167 nbdir = self.notebook_dir.name
170 168
171 169 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
172 170 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
173 171
174 172 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
175 173 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
176 174
177 175 def test_list_notebooks(self):
178 176 nbs = notebooks_only(self.api.list().json())
179 177 self.assertEqual(len(nbs), 1)
180 178 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
181 179
182 180 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
183 181 self.assertEqual(len(nbs), 1)
184 182 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
185 183
186 184 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
187 185 self.assertEqual(len(nbs), 1)
188 186 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
189 187 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
190 188
191 189 nbs = notebooks_only(self.api.list('/foo/bar/').json())
192 190 self.assertEqual(len(nbs), 1)
193 191 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
194 192 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
195 193
196 194 nbs = notebooks_only(self.api.list('foo').json())
197 195 self.assertEqual(len(nbs), 4)
198 196 nbnames = { normalize('NFC', n['name']) for n in nbs }
199 197 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
200 198 expected = { normalize('NFC', name) for name in expected }
201 199 self.assertEqual(nbnames, expected)
202 200
203 201 nbs = notebooks_only(self.api.list('ordering').json())
204 202 nbnames = [n['name'] for n in nbs]
205 203 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
206 204 self.assertEqual(nbnames, expected)
207 205
208 206 def test_list_dirs(self):
209 207 print(self.api.list().json())
210 208 dirs = dirs_only(self.api.list().json())
211 209 dir_names = {normalize('NFC', d['name']) for d in dirs}
212 210 print(dir_names)
213 211 print(self.top_level_dirs)
214 212 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
215 213
216 214 def test_list_nonexistant_dir(self):
217 215 with assert_http_error(404):
218 216 self.api.list('nonexistant')
219 217
220 218 def test_get_nb_contents(self):
221 219 for d, name in self.dirs_nbs:
222 220 path = url_path_join(d, name + '.ipynb')
223 221 nb = self.api.read(path).json()
224 222 self.assertEqual(nb['name'], u'%s.ipynb' % name)
225 223 self.assertEqual(nb['path'], path)
226 224 self.assertEqual(nb['type'], 'notebook')
227 225 self.assertIn('content', nb)
228 226 self.assertEqual(nb['format'], 'json')
229 227 self.assertIn('content', nb)
230 228 self.assertIn('metadata', nb['content'])
231 229 self.assertIsInstance(nb['content']['metadata'], dict)
232 230
233 231 def test_get_contents_no_such_file(self):
234 232 # Name that doesn't exist - should be a 404
235 233 with assert_http_error(404):
236 234 self.api.read('foo/q.ipynb')
237 235
238 236 def test_get_text_file_contents(self):
239 237 for d, name in self.dirs_nbs:
240 238 path = url_path_join(d, name + '.txt')
241 239 model = self.api.read(path).json()
242 240 self.assertEqual(model['name'], u'%s.txt' % name)
243 241 self.assertEqual(model['path'], path)
244 242 self.assertIn('content', model)
245 243 self.assertEqual(model['format'], 'text')
246 244 self.assertEqual(model['type'], 'file')
247 245 self.assertEqual(model['content'], self._txt_for_name(name))
248 246
249 247 # Name that doesn't exist - should be a 404
250 248 with assert_http_error(404):
251 249 self.api.read('foo/q.txt')
252 250
253 251 # Specifying format=text should fail on a non-UTF-8 file
254 252 with assert_http_error(400):
255 253 self.api.read('foo/bar/baz.blob', type_='file', format='text')
256 254
257 255 def test_get_binary_file_contents(self):
258 256 for d, name in self.dirs_nbs:
259 257 path = url_path_join(d, name + '.blob')
260 258 model = self.api.read(path).json()
261 259 self.assertEqual(model['name'], u'%s.blob' % name)
262 260 self.assertEqual(model['path'], path)
263 261 self.assertIn('content', model)
264 262 self.assertEqual(model['format'], 'base64')
265 263 self.assertEqual(model['type'], 'file')
266 264 b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii')
267 265 self.assertEqual(model['content'], b64_data)
268 266
269 267 # Name that doesn't exist - should be a 404
270 268 with assert_http_error(404):
271 269 self.api.read('foo/q.txt')
272 270
273 271 def test_get_bad_type(self):
274 272 with assert_http_error(400):
275 273 self.api.read(u'unicodΓ©', type_='file') # this is a directory
276 274
277 275 with assert_http_error(400):
278 276 self.api.read(u'unicodΓ©/innonascii.ipynb', type_='directory')
279 277
280 278 def _check_created(self, resp, path, type='notebook'):
281 279 self.assertEqual(resp.status_code, 201)
282 280 location_header = py3compat.str_to_unicode(resp.headers['Location'])
283 281 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
284 282 rjson = resp.json()
285 283 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
286 284 self.assertEqual(rjson['path'], path)
287 285 self.assertEqual(rjson['type'], type)
288 286 isright = os.path.isdir if type == 'directory' else os.path.isfile
289 287 assert isright(pjoin(
290 288 self.notebook_dir.name,
291 289 path.replace('/', os.sep),
292 290 ))
293 291
294 292 def test_create_untitled(self):
295 293 resp = self.api.create_untitled(path=u'Γ₯ b')
296 294 self._check_created(resp, u'Γ₯ b/Untitled0.ipynb')
297 295
298 296 # Second time
299 297 resp = self.api.create_untitled(path=u'Γ₯ b')
300 298 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
301 299
302 300 # And two directories down
303 301 resp = self.api.create_untitled(path='foo/bar')
304 302 self._check_created(resp, 'foo/bar/Untitled0.ipynb')
305 303
306 304 def test_create_untitled_txt(self):
307 305 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
308 306 self._check_created(resp, 'foo/bar/untitled0.txt', type='file')
309 307
310 308 resp = self.api.read(path='foo/bar/untitled0.txt')
311 309 model = resp.json()
312 310 self.assertEqual(model['type'], 'file')
313 311 self.assertEqual(model['format'], 'text')
314 312 self.assertEqual(model['content'], '')
315 313
316 314 def test_upload(self):
317 315 nb = new_notebook()
318 316 nbmodel = {'content': nb, 'type': 'notebook'}
319 317 path = u'Γ₯ b/Upload tΓ©st.ipynb'
320 318 resp = self.api.upload(path, body=json.dumps(nbmodel))
321 319 self._check_created(resp, path)
322 320
323 321 def test_mkdir_untitled(self):
324 322 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
325 323 self._check_created(resp, u'Γ₯ b/Untitled Folder0', type='directory')
326 324
327 325 # Second time
328 326 resp = self.api.mkdir_untitled(path=u'Γ₯ b')
329 327 self._check_created(resp, u'Γ₯ b/Untitled Folder1', type='directory')
330 328
331 329 # And two directories down
332 330 resp = self.api.mkdir_untitled(path='foo/bar')
333 331 self._check_created(resp, 'foo/bar/Untitled Folder0', type='directory')
334 332
335 333 def test_mkdir(self):
336 334 path = u'Γ₯ b/New βˆ‚ir'
337 335 resp = self.api.mkdir(path)
338 336 self._check_created(resp, path, type='directory')
339 337
340 338 def test_mkdir_hidden_400(self):
341 339 with assert_http_error(400):
342 340 resp = self.api.mkdir(u'Γ₯ b/.hidden')
343 341
344 342 def test_upload_txt(self):
345 343 body = u'ΓΌnicode tΓ©xt'
346 344 model = {
347 345 'content' : body,
348 346 'format' : 'text',
349 347 'type' : 'file',
350 348 }
351 349 path = u'Γ₯ b/Upload tΓ©st.txt'
352 350 resp = self.api.upload(path, body=json.dumps(model))
353 351
354 352 # check roundtrip
355 353 resp = self.api.read(path)
356 354 model = resp.json()
357 355 self.assertEqual(model['type'], 'file')
358 356 self.assertEqual(model['format'], 'text')
359 357 self.assertEqual(model['content'], body)
360 358
361 359 def test_upload_b64(self):
362 360 body = b'\xFFblob'
363 361 b64body = base64.encodestring(body).decode('ascii')
364 362 model = {
365 363 'content' : b64body,
366 364 'format' : 'base64',
367 365 'type' : 'file',
368 366 }
369 367 path = u'Γ₯ b/Upload tΓ©st.blob'
370 368 resp = self.api.upload(path, body=json.dumps(model))
371 369
372 370 # check roundtrip
373 371 resp = self.api.read(path)
374 372 model = resp.json()
375 373 self.assertEqual(model['type'], 'file')
376 374 self.assertEqual(model['path'], path)
377 375 self.assertEqual(model['format'], 'base64')
378 376 decoded = base64.decodestring(model['content'].encode('ascii'))
379 377 self.assertEqual(decoded, body)
380 378
381 379 def test_upload_v2(self):
382 380 nb = v2.new_notebook()
383 381 ws = v2.new_worksheet()
384 382 nb.worksheets.append(ws)
385 383 ws.cells.append(v2.new_code_cell(input='print("hi")'))
386 384 nbmodel = {'content': nb, 'type': 'notebook'}
387 385 path = u'Γ₯ b/Upload tΓ©st.ipynb'
388 386 resp = self.api.upload(path, body=json.dumps(nbmodel))
389 387 self._check_created(resp, path)
390 388 resp = self.api.read(path)
391 389 data = resp.json()
392 390 self.assertEqual(data['content']['nbformat'], 4)
393 391
394 392 def test_copy(self):
395 393 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'unicodΓ©')
396 394 self._check_created(resp, u'unicodΓ©/Γ§ d-Copy0.ipynb')
397 395
398 396 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
399 397 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy0.ipynb')
400 398
401 399 def test_copy_path(self):
402 400 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b')
403 401 self._check_created(resp, u'Γ₯ b/a-Copy0.ipynb')
404 402
405 403 def test_copy_put_400(self):
406 404 with assert_http_error(400):
407 405 resp = self.api.copy_put(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
408 406
409 407 def test_copy_dir_400(self):
410 408 # can't copy directories
411 409 with assert_http_error(400):
412 410 resp = self.api.copy(u'Γ₯ b', u'foo')
413 411
414 412 def test_delete(self):
415 413 for d, name in self.dirs_nbs:
416 414 print('%r, %r' % (d, name))
417 415 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
418 416 self.assertEqual(resp.status_code, 204)
419 417
420 418 for d in self.dirs + ['/']:
421 419 nbs = notebooks_only(self.api.list(d).json())
422 420 print('------')
423 421 print(d)
424 422 print(nbs)
425 423 self.assertEqual(nbs, [])
426 424
427 425 def test_delete_dirs(self):
428 426 # depth-first delete everything, so we don't try to delete empty directories
429 427 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
430 428 listing = self.api.list(name).json()['content']
431 429 for model in listing:
432 430 self.api.delete(model['path'])
433 431 listing = self.api.list('/').json()['content']
434 432 self.assertEqual(listing, [])
435 433
436 434 def test_delete_non_empty_dir(self):
437 435 """delete non-empty dir raises 400"""
438 436 with assert_http_error(400):
439 437 self.api.delete(u'Γ₯ b')
440 438
441 439 def test_rename(self):
442 440 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
443 441 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
444 442 self.assertEqual(resp.json()['name'], 'z.ipynb')
445 443 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
446 444 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
447 445
448 446 nbs = notebooks_only(self.api.list('foo').json())
449 447 nbnames = set(n['name'] for n in nbs)
450 448 self.assertIn('z.ipynb', nbnames)
451 449 self.assertNotIn('a.ipynb', nbnames)
452 450
453 451 def test_rename_existing(self):
454 452 with assert_http_error(409):
455 453 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
456 454
457 455 def test_save(self):
458 456 resp = self.api.read('foo/a.ipynb')
459 457 nbcontent = json.loads(resp.text)['content']
460 458 nb = from_dict(nbcontent)
461 459 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
462 460
463 461 nbmodel= {'content': nb, 'type': 'notebook'}
464 462 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
465 463
466 464 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
467 465 with io.open(nbfile, 'r', encoding='utf-8') as f:
468 466 newnb = read(f, as_version=4)
469 467 self.assertEqual(newnb.cells[0].source,
470 468 u'Created by test Β³')
471 469 nbcontent = self.api.read('foo/a.ipynb').json()['content']
472 470 newnb = from_dict(nbcontent)
473 471 self.assertEqual(newnb.cells[0].source,
474 472 u'Created by test Β³')
475 473
476 474
477 475 def test_checkpoints(self):
478 476 resp = self.api.read('foo/a.ipynb')
479 477 r = self.api.new_checkpoint('foo/a.ipynb')
480 478 self.assertEqual(r.status_code, 201)
481 479 cp1 = r.json()
482 480 self.assertEqual(set(cp1), {'id', 'last_modified'})
483 481 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
484 482
485 483 # Modify it
486 484 nbcontent = json.loads(resp.text)['content']
487 485 nb = from_dict(nbcontent)
488 486 hcell = new_markdown_cell('Created by test')
489 487 nb.cells.append(hcell)
490 488 # Save
491 489 nbmodel= {'content': nb, 'type': 'notebook'}
492 490 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
493 491
494 492 # List checkpoints
495 493 cps = self.api.get_checkpoints('foo/a.ipynb').json()
496 494 self.assertEqual(cps, [cp1])
497 495
498 496 nbcontent = self.api.read('foo/a.ipynb').json()['content']
499 497 nb = from_dict(nbcontent)
500 498 self.assertEqual(nb.cells[0].source, 'Created by test')
501 499
502 500 # Restore cp1
503 501 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
504 502 self.assertEqual(r.status_code, 204)
505 503 nbcontent = self.api.read('foo/a.ipynb').json()['content']
506 504 nb = from_dict(nbcontent)
507 505 self.assertEqual(nb.cells, [])
508 506
509 507 # Delete cp1
510 508 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
511 509 self.assertEqual(r.status_code, 204)
512 510 cps = self.api.get_checkpoints('foo/a.ipynb').json()
513 511 self.assertEqual(cps, [])
@@ -1,267 +1,267 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 9 var Contents = function(options) {
10 10 // Constructor
11 11 //
12 12 // A contents handles passing file operations
13 13 // to the back-end. This includes checkpointing
14 14 // with the normal file operations.
15 15 //
16 16 // Parameters:
17 17 // options: dictionary
18 18 // Dictionary of keyword arguments.
19 19 // base_url: string
20 20 this.base_url = options.base_url;
21 21 };
22 22
23 23 /** Error type */
24 24 Contents.DIRECTORY_NOT_EMPTY_ERROR = 'DirectoryNotEmptyError';
25 25
26 26 Contents.DirectoryNotEmptyError = function() {
27 27 // Constructor
28 28 //
29 29 // An error representing the result of attempting to delete a non-empty
30 30 // directory.
31 31 this.message = 'A directory must be empty before being deleted.';
32 32 };
33 33
34 34 Contents.DirectoryNotEmptyError.prototype = Object.create(Error.prototype);
35 35 Contents.DirectoryNotEmptyError.prototype.name =
36 36 Contents.DIRECTORY_NOT_EMPTY_ERROR;
37 37
38 38
39 39 Contents.prototype.api_url = function() {
40 40 var url_parts = [this.base_url, 'api/contents'].concat(
41 41 Array.prototype.slice.apply(arguments));
42 42 return utils.url_join_encode.apply(null, url_parts);
43 43 };
44 44
45 45 /**
46 46 * Creates a basic error handler that wraps a jqXHR error as an Error.
47 47 *
48 48 * Takes a callback that accepts an Error, and returns a callback that can
49 49 * be passed directly to $.ajax, which will wrap the error from jQuery
50 50 * as an Error, and pass that to the original callback.
51 51 *
52 52 * @method create_basic_error_handler
53 53 * @param{Function} callback
54 54 * @return{Function}
55 55 */
56 56 Contents.prototype.create_basic_error_handler = function(callback) {
57 57 if (!callback) {
58 58 return utils.log_ajax_error;
59 59 }
60 60 return function(xhr, status, error) {
61 61 callback(utils.wrap_ajax_error(xhr, status, error));
62 62 };
63 63 };
64 64
65 65 /**
66 66 * File Functions (including notebook operations)
67 67 */
68 68
69 69 /**
70 70 * Get a file.
71 71 *
72 72 * Calls success with file JSON model, or error with error.
73 73 *
74 74 * @method get
75 75 * @param {String} path
76 76 * @param {Function} success
77 77 * @param {Function} error
78 78 */
79 79 Contents.prototype.get = function (path, options) {
80 80 // We do the call with settings so we can set cache to false.
81 81 var settings = {
82 82 processData : false,
83 83 cache : false,
84 84 type : "GET",
85 85 dataType : "json",
86 86 success : options.success,
87 87 error : this.create_basic_error_handler(options.error)
88 88 };
89 89 var url = this.api_url(path);
90 if (options.type) {
91 url += '?type=' + options.type;
92 }
93 $.ajax(url, settings);
90 params = {};
91 if (options.type) { params.type = options.type; }
92 if (options.format) { params.format = options.format; }
93 $.ajax(url + '?' + $.param(params), settings);
94 94 };
95 95
96 96
97 97 /**
98 98 * Creates a new untitled file or directory in the specified directory path.
99 99 *
100 100 * @method new
101 101 * @param {String} path: the directory in which to create the new file/directory
102 102 * @param {Object} options:
103 103 * ext: file extension to use
104 104 * type: model type to create ('notebook', 'file', or 'directory')
105 105 */
106 106 Contents.prototype.new_untitled = function(path, options) {
107 107 var data = JSON.stringify({
108 108 ext: options.ext,
109 109 type: options.type
110 110 });
111 111
112 112 var settings = {
113 113 processData : false,
114 114 type : "POST",
115 115 data: data,
116 116 dataType : "json",
117 117 success : options.success || function() {},
118 118 error : this.create_basic_error_handler(options.error)
119 119 };
120 120 if (options.extra_settings) {
121 121 $.extend(settings, options.extra_settings);
122 122 }
123 123 $.ajax(this.api_url(path), settings);
124 124 };
125 125
126 126 Contents.prototype.delete = function(path, options) {
127 127 var error_callback = options.error || function() {};
128 128 var settings = {
129 129 processData : false,
130 130 type : "DELETE",
131 131 dataType : "json",
132 132 success : options.success || function() {},
133 133 error : function(xhr, status, error) {
134 134 // TODO: update IPEP27 to specify errors more precisely, so
135 135 // that error types can be detected here with certainty.
136 136 if (xhr.status === 400) {
137 137 error_callback(new Contents.DirectoryNotEmptyError());
138 138 }
139 139 error_callback(utils.wrap_ajax_error(xhr, status, error));
140 140 }
141 141 };
142 142 var url = this.api_url(path);
143 143 $.ajax(url, settings);
144 144 };
145 145
146 146 Contents.prototype.rename = function(path, new_path, options) {
147 147 var data = {path: new_path};
148 148 var settings = {
149 149 processData : false,
150 150 type : "PATCH",
151 151 data : JSON.stringify(data),
152 152 dataType: "json",
153 153 contentType: 'application/json',
154 154 success : options.success || function() {},
155 155 error : this.create_basic_error_handler(options.error)
156 156 };
157 157 var url = this.api_url(path);
158 158 $.ajax(url, settings);
159 159 };
160 160
161 161 Contents.prototype.save = function(path, model, options) {
162 162 // We do the call with settings so we can set cache to false.
163 163 var settings = {
164 164 processData : false,
165 165 type : "PUT",
166 166 data : JSON.stringify(model),
167 167 contentType: 'application/json',
168 168 success : options.success || function() {},
169 169 error : this.create_basic_error_handler(options.error)
170 170 };
171 171 if (options.extra_settings) {
172 172 $.extend(settings, options.extra_settings);
173 173 }
174 174 var url = this.api_url(path);
175 175 $.ajax(url, settings);
176 176 };
177 177
178 178 Contents.prototype.copy = function(from_file, to_dir, options) {
179 179 // Copy a file into a given directory via POST
180 180 // The server will select the name of the copied file
181 181 var url = this.api_url(to_dir);
182 182
183 183 var settings = {
184 184 processData : false,
185 185 type: "POST",
186 186 data: JSON.stringify({copy_from: from_file}),
187 187 dataType : "json",
188 188 success: options.success || function() {},
189 189 error: this.create_basic_error_handler(options.error)
190 190 };
191 191 if (options.extra_settings) {
192 192 $.extend(settings, options.extra_settings);
193 193 }
194 194 $.ajax(url, settings);
195 195 };
196 196
197 197 /**
198 198 * Checkpointing Functions
199 199 */
200 200
201 201 Contents.prototype.create_checkpoint = function(path, options) {
202 202 var url = this.api_url(path, 'checkpoints');
203 203 var settings = {
204 204 type : "POST",
205 205 success: options.success || function() {},
206 206 error : this.create_basic_error_handler(options.error)
207 207 };
208 208 $.ajax(url, settings);
209 209 };
210 210
211 211 Contents.prototype.list_checkpoints = function(path, options) {
212 212 var url = this.api_url(path, 'checkpoints');
213 213 var settings = {
214 214 type : "GET",
215 215 success: options.success,
216 216 error : this.create_basic_error_handler(options.error)
217 217 };
218 218 $.ajax(url, settings);
219 219 };
220 220
221 221 Contents.prototype.restore_checkpoint = function(path, checkpoint_id, options) {
222 222 var url = this.api_url(path, 'checkpoints', checkpoint_id);
223 223 var settings = {
224 224 type : "POST",
225 225 success: options.success || function() {},
226 226 error : this.create_basic_error_handler(options.error)
227 227 };
228 228 $.ajax(url, settings);
229 229 };
230 230
231 231 Contents.prototype.delete_checkpoint = function(path, checkpoint_id, options) {
232 232 var url = this.api_url(path, 'checkpoints', checkpoint_id);
233 233 var settings = {
234 234 type : "DELETE",
235 235 success: options.success || function() {},
236 236 error : this.create_basic_error_handler(options.error)
237 237 };
238 238 $.ajax(url, settings);
239 239 };
240 240
241 241 /**
242 242 * File management functions
243 243 */
244 244
245 245 /**
246 246 * List notebooks and directories at a given path
247 247 *
248 248 * On success, load_callback is called with an array of dictionaries
249 249 * representing individual files or directories. Each dictionary has
250 250 * the keys:
251 251 * type: "notebook" or "directory"
252 252 * created: created date
253 253 * last_modified: last modified dat
254 254 * @method list_notebooks
255 255 * @param {String} path The path to list notebooks in
256 256 * @param {Object} options including success and error callbacks
257 257 */
258 258 Contents.prototype.list_contents = function(path, options) {
259 259 options.type = 'directory';
260 260 this.get(path, options);
261 261 };
262 262
263 263
264 264 IPython.Contents = Contents;
265 265
266 266 return {'Contents': Contents};
267 267 });
General Comments 0
You need to be logged in to leave comments. Login now