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