##// END OF EJS Templates
Merge pull request #5357 from minrk/smb-fails...
Thomas Kluyver -
r15872:2a9ed0e9 merge
parent child Browse files
Show More
@@ -1,483 +1,494
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 24
25 25 from tornado import web
26 26
27 27 from .nbmanager import NotebookManager
28 28 from IPython.nbformat import current
29 29 from IPython.utils.traitlets import Unicode, Bool, TraitError
30 30 from IPython.utils.py3compat import getcwd
31 31 from IPython.utils import tz
32 32 from IPython.html.utils import is_hidden, to_os_path
33 33
34 34 def sort_key(item):
35 35 """Case-insensitive sorting."""
36 36 return item['name'].lower()
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class FileNotebookManager(NotebookManager):
43 43
44 44 save_script = Bool(False, config=True,
45 45 help="""Automatically create a Python script when saving the notebook.
46 46
47 47 For easier use of import, %run and %load across notebooks, a
48 48 <notebook-name>.py script will be created next to any
49 49 <notebook-name>.ipynb on each save. This can also be set with the
50 50 short `--script` flag.
51 51 """
52 52 )
53 53 notebook_dir = Unicode(getcwd(), config=True)
54 54
55 55 def _notebook_dir_changed(self, name, old, new):
56 56 """Do a bit of validation of the notebook dir."""
57 57 if not os.path.isabs(new):
58 58 # If we receive a non-absolute path, make it absolute.
59 59 self.notebook_dir = os.path.abspath(new)
60 60 return
61 61 if not os.path.exists(new) or not os.path.isdir(new):
62 62 raise TraitError("notebook dir %r is not a directory" % new)
63 63
64 64 checkpoint_dir = Unicode(config=True,
65 65 help="""The location in which to keep notebook checkpoints
66 66
67 67 By default, it is notebook-dir/.ipynb_checkpoints
68 68 """
69 69 )
70 70 def _checkpoint_dir_default(self):
71 71 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
72 72
73 73 def _checkpoint_dir_changed(self, name, old, new):
74 74 """do a bit of validation of the checkpoint dir"""
75 75 if not os.path.isabs(new):
76 76 # If we receive a non-absolute path, make it absolute.
77 77 abs_new = os.path.abspath(new)
78 78 self.checkpoint_dir = abs_new
79 79 return
80 80 if os.path.exists(new) and not os.path.isdir(new):
81 81 raise TraitError("checkpoint dir %r is not a directory" % new)
82 82 if not os.path.exists(new):
83 83 self.log.info("Creating checkpoint dir %s", new)
84 84 try:
85 85 os.mkdir(new)
86 86 except:
87 87 raise TraitError("Couldn't create checkpoint dir %r" % new)
88
88
89 def _copy(self, src, dest):
90 """copy src to dest
91
92 like shutil.copy2, but log errors in copystat
93 """
94 shutil.copyfile(src, dest)
95 try:
96 shutil.copystat(src, dest)
97 except OSError as e:
98 self.log.debug("copystat on %s failed", dest, exc_info=True)
99
89 100 def get_notebook_names(self, path=''):
90 101 """List all notebook names in the notebook dir and path."""
91 102 path = path.strip('/')
92 103 if not os.path.isdir(self._get_os_path(path=path)):
93 104 raise web.HTTPError(404, 'Directory not found: ' + path)
94 105 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
95 106 names = [os.path.basename(name)
96 107 for name in names]
97 108 return names
98 109
99 110 def path_exists(self, path):
100 111 """Does the API-style path (directory) actually exist?
101 112
102 113 Parameters
103 114 ----------
104 115 path : string
105 116 The path to check. This is an API path (`/` separated,
106 117 relative to base notebook-dir).
107 118
108 119 Returns
109 120 -------
110 121 exists : bool
111 122 Whether the path is indeed a directory.
112 123 """
113 124 path = path.strip('/')
114 125 os_path = self._get_os_path(path=path)
115 126 return os.path.isdir(os_path)
116 127
117 128 def is_hidden(self, path):
118 129 """Does the API style path correspond to a hidden directory or file?
119 130
120 131 Parameters
121 132 ----------
122 133 path : string
123 134 The path to check. This is an API path (`/` separated,
124 135 relative to base notebook-dir).
125 136
126 137 Returns
127 138 -------
128 139 exists : bool
129 140 Whether the path is hidden.
130 141
131 142 """
132 143 path = path.strip('/')
133 144 os_path = self._get_os_path(path=path)
134 145 return is_hidden(os_path, self.notebook_dir)
135 146
136 147 def _get_os_path(self, name=None, path=''):
137 148 """Given a notebook name and a URL path, return its file system
138 149 path.
139 150
140 151 Parameters
141 152 ----------
142 153 name : string
143 154 The name of a notebook file with the .ipynb extension
144 155 path : string
145 156 The relative URL path (with '/' as separator) to the named
146 157 notebook.
147 158
148 159 Returns
149 160 -------
150 161 path : string
151 162 A file system path that combines notebook_dir (location where
152 163 server started), the relative path, and the filename with the
153 164 current operating system's url.
154 165 """
155 166 if name is not None:
156 167 path = path + '/' + name
157 168 return to_os_path(path, self.notebook_dir)
158 169
159 170 def notebook_exists(self, name, path=''):
160 171 """Returns a True if the notebook exists. Else, returns False.
161 172
162 173 Parameters
163 174 ----------
164 175 name : string
165 176 The name of the notebook you are checking.
166 177 path : string
167 178 The relative path to the notebook (with '/' as separator)
168 179
169 180 Returns
170 181 -------
171 182 bool
172 183 """
173 184 path = path.strip('/')
174 185 nbpath = self._get_os_path(name, path=path)
175 186 return os.path.isfile(nbpath)
176 187
177 188 # TODO: Remove this after we create the contents web service and directories are
178 189 # no longer listed by the notebook web service.
179 190 def list_dirs(self, path):
180 191 """List the directories for a given API style path."""
181 192 path = path.strip('/')
182 193 os_path = self._get_os_path('', path)
183 194 if not os.path.isdir(os_path):
184 195 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
185 196 elif is_hidden(os_path, self.notebook_dir):
186 197 self.log.info("Refusing to serve hidden directory, via 404 Error")
187 198 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
188 199 dir_names = os.listdir(os_path)
189 200 dirs = []
190 201 for name in dir_names:
191 202 os_path = self._get_os_path(name, path)
192 203 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
193 204 and self.should_list(name):
194 205 try:
195 206 model = self.get_dir_model(name, path)
196 207 except IOError:
197 208 pass
198 209 dirs.append(model)
199 210 dirs = sorted(dirs, key=sort_key)
200 211 return dirs
201 212
202 213 # TODO: Remove this after we create the contents web service and directories are
203 214 # no longer listed by the notebook web service.
204 215 def get_dir_model(self, name, path=''):
205 216 """Get the directory model given a directory name and its API style path"""
206 217 path = path.strip('/')
207 218 os_path = self._get_os_path(name, path)
208 219 if not os.path.isdir(os_path):
209 220 raise IOError('directory does not exist: %r' % os_path)
210 221 info = os.stat(os_path)
211 222 last_modified = tz.utcfromtimestamp(info.st_mtime)
212 223 created = tz.utcfromtimestamp(info.st_ctime)
213 224 # Create the notebook model.
214 225 model ={}
215 226 model['name'] = name
216 227 model['path'] = path
217 228 model['last_modified'] = last_modified
218 229 model['created'] = created
219 230 model['type'] = 'directory'
220 231 return model
221 232
222 233 def list_notebooks(self, path):
223 234 """Returns a list of dictionaries that are the standard model
224 235 for all notebooks in the relative 'path'.
225 236
226 237 Parameters
227 238 ----------
228 239 path : str
229 240 the URL path that describes the relative path for the
230 241 listed notebooks
231 242
232 243 Returns
233 244 -------
234 245 notebooks : list of dicts
235 246 a list of the notebook models without 'content'
236 247 """
237 248 path = path.strip('/')
238 249 notebook_names = self.get_notebook_names(path)
239 250 notebooks = [self.get_notebook(name, path, content=False)
240 251 for name in notebook_names if self.should_list(name)]
241 252 notebooks = sorted(notebooks, key=sort_key)
242 253 return notebooks
243 254
244 255 def get_notebook(self, name, path='', content=True):
245 256 """ Takes a path and name for a notebook and returns its model
246 257
247 258 Parameters
248 259 ----------
249 260 name : str
250 261 the name of the notebook
251 262 path : str
252 263 the URL path that describes the relative path for
253 264 the notebook
254 265
255 266 Returns
256 267 -------
257 268 model : dict
258 269 the notebook model. If contents=True, returns the 'contents'
259 270 dict in the model as well.
260 271 """
261 272 path = path.strip('/')
262 273 if not self.notebook_exists(name=name, path=path):
263 274 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
264 275 os_path = self._get_os_path(name, path)
265 276 info = os.stat(os_path)
266 277 last_modified = tz.utcfromtimestamp(info.st_mtime)
267 278 created = tz.utcfromtimestamp(info.st_ctime)
268 279 # Create the notebook model.
269 280 model ={}
270 281 model['name'] = name
271 282 model['path'] = path
272 283 model['last_modified'] = last_modified
273 284 model['created'] = created
274 285 model['type'] = 'notebook'
275 286 if content:
276 287 with io.open(os_path, 'r', encoding='utf-8') as f:
277 288 try:
278 289 nb = current.read(f, u'json')
279 290 except Exception as e:
280 291 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
281 292 self.mark_trusted_cells(nb, name, path)
282 293 model['content'] = nb
283 294 return model
284 295
285 296 def save_notebook(self, model, name='', path=''):
286 297 """Save the notebook model and return the model with no content."""
287 298 path = path.strip('/')
288 299
289 300 if 'content' not in model:
290 301 raise web.HTTPError(400, u'No notebook JSON data provided')
291 302
292 303 # One checkpoint should always exist
293 304 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
294 305 self.create_checkpoint(name, path)
295 306
296 307 new_path = model.get('path', path).strip('/')
297 308 new_name = model.get('name', name)
298 309
299 310 if path != new_path or name != new_name:
300 311 self.rename_notebook(name, path, new_name, new_path)
301 312
302 313 # Save the notebook file
303 314 os_path = self._get_os_path(new_name, new_path)
304 315 nb = current.to_notebook_json(model['content'])
305 316
306 317 self.check_and_sign(nb, new_name, new_path)
307 318
308 319 if 'name' in nb['metadata']:
309 320 nb['metadata']['name'] = u''
310 321 try:
311 322 self.log.debug("Autosaving notebook %s", os_path)
312 323 with io.open(os_path, 'w', encoding='utf-8') as f:
313 324 current.write(nb, f, u'json')
314 325 except Exception as e:
315 326 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
316 327
317 328 # Save .py script as well
318 329 if self.save_script:
319 330 py_path = os.path.splitext(os_path)[0] + '.py'
320 331 self.log.debug("Writing script %s", py_path)
321 332 try:
322 333 with io.open(py_path, 'w', encoding='utf-8') as f:
323 334 current.write(nb, f, u'py')
324 335 except Exception as e:
325 336 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
326 337
327 338 model = self.get_notebook(new_name, new_path, content=False)
328 339 return model
329 340
330 341 def update_notebook(self, model, name, path=''):
331 342 """Update the notebook's path and/or name"""
332 343 path = path.strip('/')
333 344 new_name = model.get('name', name)
334 345 new_path = model.get('path', path).strip('/')
335 346 if path != new_path or name != new_name:
336 347 self.rename_notebook(name, path, new_name, new_path)
337 348 model = self.get_notebook(new_name, new_path, content=False)
338 349 return model
339 350
340 351 def delete_notebook(self, name, path=''):
341 352 """Delete notebook by name and path."""
342 353 path = path.strip('/')
343 354 os_path = self._get_os_path(name, path)
344 355 if not os.path.isfile(os_path):
345 356 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
346 357
347 358 # clear checkpoints
348 359 for checkpoint in self.list_checkpoints(name, path):
349 360 checkpoint_id = checkpoint['id']
350 361 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
351 362 if os.path.isfile(cp_path):
352 363 self.log.debug("Unlinking checkpoint %s", cp_path)
353 364 os.unlink(cp_path)
354 365
355 366 self.log.debug("Unlinking notebook %s", os_path)
356 367 os.unlink(os_path)
357 368
358 369 def rename_notebook(self, old_name, old_path, new_name, new_path):
359 370 """Rename a notebook."""
360 371 old_path = old_path.strip('/')
361 372 new_path = new_path.strip('/')
362 373 if new_name == old_name and new_path == old_path:
363 374 return
364 375
365 376 new_os_path = self._get_os_path(new_name, new_path)
366 377 old_os_path = self._get_os_path(old_name, old_path)
367 378
368 379 # Should we proceed with the move?
369 380 if os.path.isfile(new_os_path):
370 381 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
371 382 if self.save_script:
372 383 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
373 384 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
374 385 if os.path.isfile(new_py_path):
375 386 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
376 387
377 388 # Move the notebook file
378 389 try:
379 390 os.rename(old_os_path, new_os_path)
380 391 except Exception as e:
381 392 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
382 393
383 394 # Move the checkpoints
384 395 old_checkpoints = self.list_checkpoints(old_name, old_path)
385 396 for cp in old_checkpoints:
386 397 checkpoint_id = cp['id']
387 398 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
388 399 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
389 400 if os.path.isfile(old_cp_path):
390 401 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
391 402 os.rename(old_cp_path, new_cp_path)
392 403
393 404 # Move the .py script
394 405 if self.save_script:
395 406 os.rename(old_py_path, new_py_path)
396 407
397 408 # Checkpoint-related utilities
398 409
399 410 def get_checkpoint_path(self, checkpoint_id, name, path=''):
400 411 """find the path to a checkpoint"""
401 412 path = path.strip('/')
402 413 basename, _ = os.path.splitext(name)
403 414 filename = u"{name}-{checkpoint_id}{ext}".format(
404 415 name=basename,
405 416 checkpoint_id=checkpoint_id,
406 417 ext=self.filename_ext,
407 418 )
408 419 cp_path = os.path.join(path, self.checkpoint_dir, filename)
409 420 return cp_path
410 421
411 422 def get_checkpoint_model(self, checkpoint_id, name, path=''):
412 423 """construct the info dict for a given checkpoint"""
413 424 path = path.strip('/')
414 425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
415 426 stats = os.stat(cp_path)
416 427 last_modified = tz.utcfromtimestamp(stats.st_mtime)
417 428 info = dict(
418 429 id = checkpoint_id,
419 430 last_modified = last_modified,
420 431 )
421 432 return info
422 433
423 434 # public checkpoint API
424 435
425 436 def create_checkpoint(self, name, path=''):
426 437 """Create a checkpoint from the current state of a notebook"""
427 438 path = path.strip('/')
428 439 nb_path = self._get_os_path(name, path)
429 440 # only the one checkpoint ID:
430 441 checkpoint_id = u"checkpoint"
431 442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
432 443 self.log.debug("creating checkpoint for notebook %s", name)
433 444 if not os.path.exists(self.checkpoint_dir):
434 445 os.mkdir(self.checkpoint_dir)
435 shutil.copy2(nb_path, cp_path)
446 self._copy(nb_path, cp_path)
436 447
437 448 # return the checkpoint info
438 449 return self.get_checkpoint_model(checkpoint_id, name, path)
439 450
440 451 def list_checkpoints(self, name, path=''):
441 452 """list the checkpoints for a given notebook
442 453
443 454 This notebook manager currently only supports one checkpoint per notebook.
444 455 """
445 456 path = path.strip('/')
446 457 checkpoint_id = "checkpoint"
447 458 path = self.get_checkpoint_path(checkpoint_id, name, path)
448 459 if not os.path.exists(path):
449 460 return []
450 461 else:
451 462 return [self.get_checkpoint_model(checkpoint_id, name, path)]
452 463
453 464
454 465 def restore_checkpoint(self, checkpoint_id, name, path=''):
455 466 """restore a notebook to a checkpointed state"""
456 467 path = path.strip('/')
457 468 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
458 469 nb_path = self._get_os_path(name, path)
459 470 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
460 471 if not os.path.isfile(cp_path):
461 472 self.log.debug("checkpoint file does not exist: %s", cp_path)
462 473 raise web.HTTPError(404,
463 474 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
464 475 )
465 476 # ensure notebook is readable (never restore from an unreadable notebook)
466 477 with io.open(cp_path, 'r', encoding='utf-8') as f:
467 478 current.read(f, u'json')
468 shutil.copy2(cp_path, nb_path)
479 self._copy(cp_path, nb_path)
469 480 self.log.debug("copying %s -> %s", cp_path, nb_path)
470 481
471 482 def delete_checkpoint(self, checkpoint_id, name, path=''):
472 483 """delete a notebook's checkpoint"""
473 484 path = path.strip('/')
474 485 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
475 486 if not os.path.isfile(cp_path):
476 487 raise web.HTTPError(404,
477 488 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
478 489 )
479 490 self.log.debug("unlinking %s", cp_path)
480 491 os.unlink(cp_path)
481 492
482 493 def info_string(self):
483 494 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now