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