##// END OF EJS Templates
Backport PR #5548: FileNotebookManager: Use shutil.move() instead of os.rename()...
MinRK -
Show More
@@ -1,494 +1,494 b''
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 89 def _copy(self, src, dest):
90 90 """copy src to dest
91 91
92 92 like shutil.copy2, but log errors in copystat
93 93 """
94 94 shutil.copyfile(src, dest)
95 95 try:
96 96 shutil.copystat(src, dest)
97 97 except OSError as e:
98 98 self.log.debug("copystat on %s failed", dest, exc_info=True)
99 99
100 100 def get_notebook_names(self, path=''):
101 101 """List all notebook names in the notebook dir and path."""
102 102 path = path.strip('/')
103 103 if not os.path.isdir(self._get_os_path(path=path)):
104 104 raise web.HTTPError(404, 'Directory not found: ' + path)
105 105 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
106 106 names = [os.path.basename(name)
107 107 for name in names]
108 108 return names
109 109
110 110 def path_exists(self, path):
111 111 """Does the API-style path (directory) actually exist?
112 112
113 113 Parameters
114 114 ----------
115 115 path : string
116 116 The path to check. This is an API path (`/` separated,
117 117 relative to base notebook-dir).
118 118
119 119 Returns
120 120 -------
121 121 exists : bool
122 122 Whether the path is indeed a directory.
123 123 """
124 124 path = path.strip('/')
125 125 os_path = self._get_os_path(path=path)
126 126 return os.path.isdir(os_path)
127 127
128 128 def is_hidden(self, path):
129 129 """Does the API style path correspond to a hidden directory or file?
130 130
131 131 Parameters
132 132 ----------
133 133 path : string
134 134 The path to check. This is an API path (`/` separated,
135 135 relative to base notebook-dir).
136 136
137 137 Returns
138 138 -------
139 139 exists : bool
140 140 Whether the path is hidden.
141 141
142 142 """
143 143 path = path.strip('/')
144 144 os_path = self._get_os_path(path=path)
145 145 return is_hidden(os_path, self.notebook_dir)
146 146
147 147 def _get_os_path(self, name=None, path=''):
148 148 """Given a notebook name and a URL path, return its file system
149 149 path.
150 150
151 151 Parameters
152 152 ----------
153 153 name : string
154 154 The name of a notebook file with the .ipynb extension
155 155 path : string
156 156 The relative URL path (with '/' as separator) to the named
157 157 notebook.
158 158
159 159 Returns
160 160 -------
161 161 path : string
162 162 A file system path that combines notebook_dir (location where
163 163 server started), the relative path, and the filename with the
164 164 current operating system's url.
165 165 """
166 166 if name is not None:
167 167 path = path + '/' + name
168 168 return to_os_path(path, self.notebook_dir)
169 169
170 170 def notebook_exists(self, name, path=''):
171 171 """Returns a True if the notebook exists. Else, returns False.
172 172
173 173 Parameters
174 174 ----------
175 175 name : string
176 176 The name of the notebook you are checking.
177 177 path : string
178 178 The relative path to the notebook (with '/' as separator)
179 179
180 180 Returns
181 181 -------
182 182 bool
183 183 """
184 184 path = path.strip('/')
185 185 nbpath = self._get_os_path(name, path=path)
186 186 return os.path.isfile(nbpath)
187 187
188 188 # TODO: Remove this after we create the contents web service and directories are
189 189 # no longer listed by the notebook web service.
190 190 def list_dirs(self, path):
191 191 """List the directories for a given API style path."""
192 192 path = path.strip('/')
193 193 os_path = self._get_os_path('', path)
194 194 if not os.path.isdir(os_path):
195 195 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
196 196 elif is_hidden(os_path, self.notebook_dir):
197 197 self.log.info("Refusing to serve hidden directory, via 404 Error")
198 198 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
199 199 dir_names = os.listdir(os_path)
200 200 dirs = []
201 201 for name in dir_names:
202 202 os_path = self._get_os_path(name, path)
203 203 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
204 204 and self.should_list(name):
205 205 try:
206 206 model = self.get_dir_model(name, path)
207 207 except IOError:
208 208 pass
209 209 dirs.append(model)
210 210 dirs = sorted(dirs, key=sort_key)
211 211 return dirs
212 212
213 213 # TODO: Remove this after we create the contents web service and directories are
214 214 # no longer listed by the notebook web service.
215 215 def get_dir_model(self, name, path=''):
216 216 """Get the directory model given a directory name and its API style path"""
217 217 path = path.strip('/')
218 218 os_path = self._get_os_path(name, path)
219 219 if not os.path.isdir(os_path):
220 220 raise IOError('directory does not exist: %r' % os_path)
221 221 info = os.stat(os_path)
222 222 last_modified = tz.utcfromtimestamp(info.st_mtime)
223 223 created = tz.utcfromtimestamp(info.st_ctime)
224 224 # Create the notebook model.
225 225 model ={}
226 226 model['name'] = name
227 227 model['path'] = path
228 228 model['last_modified'] = last_modified
229 229 model['created'] = created
230 230 model['type'] = 'directory'
231 231 return model
232 232
233 233 def list_notebooks(self, path):
234 234 """Returns a list of dictionaries that are the standard model
235 235 for all notebooks in the relative 'path'.
236 236
237 237 Parameters
238 238 ----------
239 239 path : str
240 240 the URL path that describes the relative path for the
241 241 listed notebooks
242 242
243 243 Returns
244 244 -------
245 245 notebooks : list of dicts
246 246 a list of the notebook models without 'content'
247 247 """
248 248 path = path.strip('/')
249 249 notebook_names = self.get_notebook_names(path)
250 250 notebooks = [self.get_notebook(name, path, content=False)
251 251 for name in notebook_names if self.should_list(name)]
252 252 notebooks = sorted(notebooks, key=sort_key)
253 253 return notebooks
254 254
255 255 def get_notebook(self, name, path='', content=True):
256 256 """ Takes a path and name for a notebook and returns its model
257 257
258 258 Parameters
259 259 ----------
260 260 name : str
261 261 the name of the notebook
262 262 path : str
263 263 the URL path that describes the relative path for
264 264 the notebook
265 265
266 266 Returns
267 267 -------
268 268 model : dict
269 269 the notebook model. If contents=True, returns the 'contents'
270 270 dict in the model as well.
271 271 """
272 272 path = path.strip('/')
273 273 if not self.notebook_exists(name=name, path=path):
274 274 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
275 275 os_path = self._get_os_path(name, path)
276 276 info = os.stat(os_path)
277 277 last_modified = tz.utcfromtimestamp(info.st_mtime)
278 278 created = tz.utcfromtimestamp(info.st_ctime)
279 279 # Create the notebook model.
280 280 model ={}
281 281 model['name'] = name
282 282 model['path'] = path
283 283 model['last_modified'] = last_modified
284 284 model['created'] = created
285 285 model['type'] = 'notebook'
286 286 if content:
287 287 with io.open(os_path, 'r', encoding='utf-8') as f:
288 288 try:
289 289 nb = current.read(f, u'json')
290 290 except Exception as e:
291 291 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
292 292 self.mark_trusted_cells(nb, name, path)
293 293 model['content'] = nb
294 294 return model
295 295
296 296 def save_notebook(self, model, name='', path=''):
297 297 """Save the notebook model and return the model with no content."""
298 298 path = path.strip('/')
299 299
300 300 if 'content' not in model:
301 301 raise web.HTTPError(400, u'No notebook JSON data provided')
302 302
303 303 # One checkpoint should always exist
304 304 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
305 305 self.create_checkpoint(name, path)
306 306
307 307 new_path = model.get('path', path).strip('/')
308 308 new_name = model.get('name', name)
309 309
310 310 if path != new_path or name != new_name:
311 311 self.rename_notebook(name, path, new_name, new_path)
312 312
313 313 # Save the notebook file
314 314 os_path = self._get_os_path(new_name, new_path)
315 315 nb = current.to_notebook_json(model['content'])
316 316
317 317 self.check_and_sign(nb, new_name, new_path)
318 318
319 319 if 'name' in nb['metadata']:
320 320 nb['metadata']['name'] = u''
321 321 try:
322 322 self.log.debug("Autosaving notebook %s", os_path)
323 323 with io.open(os_path, 'w', encoding='utf-8') as f:
324 324 current.write(nb, f, u'json')
325 325 except Exception as e:
326 326 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
327 327
328 328 # Save .py script as well
329 329 if self.save_script:
330 330 py_path = os.path.splitext(os_path)[0] + '.py'
331 331 self.log.debug("Writing script %s", py_path)
332 332 try:
333 333 with io.open(py_path, 'w', encoding='utf-8') as f:
334 334 current.write(nb, f, u'py')
335 335 except Exception as e:
336 336 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
337 337
338 338 model = self.get_notebook(new_name, new_path, content=False)
339 339 return model
340 340
341 341 def update_notebook(self, model, name, path=''):
342 342 """Update the notebook's path and/or name"""
343 343 path = path.strip('/')
344 344 new_name = model.get('name', name)
345 345 new_path = model.get('path', path).strip('/')
346 346 if path != new_path or name != new_name:
347 347 self.rename_notebook(name, path, new_name, new_path)
348 348 model = self.get_notebook(new_name, new_path, content=False)
349 349 return model
350 350
351 351 def delete_notebook(self, name, path=''):
352 352 """Delete notebook by name and path."""
353 353 path = path.strip('/')
354 354 os_path = self._get_os_path(name, path)
355 355 if not os.path.isfile(os_path):
356 356 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
357 357
358 358 # clear checkpoints
359 359 for checkpoint in self.list_checkpoints(name, path):
360 360 checkpoint_id = checkpoint['id']
361 361 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
362 362 if os.path.isfile(cp_path):
363 363 self.log.debug("Unlinking checkpoint %s", cp_path)
364 364 os.unlink(cp_path)
365 365
366 366 self.log.debug("Unlinking notebook %s", os_path)
367 367 os.unlink(os_path)
368 368
369 369 def rename_notebook(self, old_name, old_path, new_name, new_path):
370 370 """Rename a notebook."""
371 371 old_path = old_path.strip('/')
372 372 new_path = new_path.strip('/')
373 373 if new_name == old_name and new_path == old_path:
374 374 return
375 375
376 376 new_os_path = self._get_os_path(new_name, new_path)
377 377 old_os_path = self._get_os_path(old_name, old_path)
378 378
379 379 # Should we proceed with the move?
380 380 if os.path.isfile(new_os_path):
381 381 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
382 382 if self.save_script:
383 383 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
384 384 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
385 385 if os.path.isfile(new_py_path):
386 386 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
387 387
388 388 # Move the notebook file
389 389 try:
390 os.rename(old_os_path, new_os_path)
390 shutil.move(old_os_path, new_os_path)
391 391 except Exception as e:
392 392 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
393 393
394 394 # Move the checkpoints
395 395 old_checkpoints = self.list_checkpoints(old_name, old_path)
396 396 for cp in old_checkpoints:
397 397 checkpoint_id = cp['id']
398 398 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
399 399 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
400 400 if os.path.isfile(old_cp_path):
401 401 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
402 os.rename(old_cp_path, new_cp_path)
402 shutil.move(old_cp_path, new_cp_path)
403 403
404 404 # Move the .py script
405 405 if self.save_script:
406 os.rename(old_py_path, new_py_path)
406 shutil.move(old_py_path, new_py_path)
407 407
408 408 # Checkpoint-related utilities
409 409
410 410 def get_checkpoint_path(self, checkpoint_id, name, path=''):
411 411 """find the path to a checkpoint"""
412 412 path = path.strip('/')
413 413 basename, _ = os.path.splitext(name)
414 414 filename = u"{name}-{checkpoint_id}{ext}".format(
415 415 name=basename,
416 416 checkpoint_id=checkpoint_id,
417 417 ext=self.filename_ext,
418 418 )
419 419 cp_path = os.path.join(path, self.checkpoint_dir, filename)
420 420 return cp_path
421 421
422 422 def get_checkpoint_model(self, checkpoint_id, name, path=''):
423 423 """construct the info dict for a given checkpoint"""
424 424 path = path.strip('/')
425 425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
426 426 stats = os.stat(cp_path)
427 427 last_modified = tz.utcfromtimestamp(stats.st_mtime)
428 428 info = dict(
429 429 id = checkpoint_id,
430 430 last_modified = last_modified,
431 431 )
432 432 return info
433 433
434 434 # public checkpoint API
435 435
436 436 def create_checkpoint(self, name, path=''):
437 437 """Create a checkpoint from the current state of a notebook"""
438 438 path = path.strip('/')
439 439 nb_path = self._get_os_path(name, path)
440 440 # only the one checkpoint ID:
441 441 checkpoint_id = u"checkpoint"
442 442 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
443 443 self.log.debug("creating checkpoint for notebook %s", name)
444 444 if not os.path.exists(self.checkpoint_dir):
445 445 os.mkdir(self.checkpoint_dir)
446 446 self._copy(nb_path, cp_path)
447 447
448 448 # return the checkpoint info
449 449 return self.get_checkpoint_model(checkpoint_id, name, path)
450 450
451 451 def list_checkpoints(self, name, path=''):
452 452 """list the checkpoints for a given notebook
453 453
454 454 This notebook manager currently only supports one checkpoint per notebook.
455 455 """
456 456 path = path.strip('/')
457 457 checkpoint_id = "checkpoint"
458 458 path = self.get_checkpoint_path(checkpoint_id, name, path)
459 459 if not os.path.exists(path):
460 460 return []
461 461 else:
462 462 return [self.get_checkpoint_model(checkpoint_id, name, path)]
463 463
464 464
465 465 def restore_checkpoint(self, checkpoint_id, name, path=''):
466 466 """restore a notebook to a checkpointed state"""
467 467 path = path.strip('/')
468 468 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
469 469 nb_path = self._get_os_path(name, path)
470 470 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
471 471 if not os.path.isfile(cp_path):
472 472 self.log.debug("checkpoint file does not exist: %s", cp_path)
473 473 raise web.HTTPError(404,
474 474 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
475 475 )
476 476 # ensure notebook is readable (never restore from an unreadable notebook)
477 477 with io.open(cp_path, 'r', encoding='utf-8') as f:
478 478 current.read(f, u'json')
479 479 self._copy(cp_path, nb_path)
480 480 self.log.debug("copying %s -> %s", cp_path, nb_path)
481 481
482 482 def delete_checkpoint(self, checkpoint_id, name, path=''):
483 483 """delete a notebook's checkpoint"""
484 484 path = path.strip('/')
485 485 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
486 486 if not os.path.isfile(cp_path):
487 487 raise web.HTTPError(404,
488 488 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
489 489 )
490 490 self.log.debug("unlinking %s", cp_path)
491 491 os.unlink(cp_path)
492 492
493 493 def info_string(self):
494 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