##// END OF EJS Templates
Make hidden directories configurable
Thomas Kluyver -
Show More
@@ -1,479 +1,480 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, Dict, Bool, TraitError
29 from IPython.utils.traitlets import Unicode, Dict, 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 get_notebook_names(self, path=''):
89 def get_notebook_names(self, path=''):
90 """List all notebook names in the notebook dir and path."""
90 """List all notebook names in the notebook dir and path."""
91 path = path.strip('/')
91 path = path.strip('/')
92 if not os.path.isdir(self._get_os_path(path=path)):
92 if not os.path.isdir(self._get_os_path(path=path)):
93 raise web.HTTPError(404, 'Directory not found: ' + path)
93 raise web.HTTPError(404, 'Directory not found: ' + path)
94 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
94 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
95 names = [os.path.basename(name)
95 names = [os.path.basename(name)
96 for name in names]
96 for name in names]
97 return names
97 return names
98
98
99 def path_exists(self, path):
99 def path_exists(self, path):
100 """Does the API-style path (directory) actually exist?
100 """Does the API-style path (directory) actually exist?
101
101
102 Parameters
102 Parameters
103 ----------
103 ----------
104 path : string
104 path : string
105 The path to check. This is an API path (`/` separated,
105 The path to check. This is an API path (`/` separated,
106 relative to base notebook-dir).
106 relative to base notebook-dir).
107
107
108 Returns
108 Returns
109 -------
109 -------
110 exists : bool
110 exists : bool
111 Whether the path is indeed a directory.
111 Whether the path is indeed a directory.
112 """
112 """
113 path = path.strip('/')
113 path = path.strip('/')
114 os_path = self._get_os_path(path=path)
114 os_path = self._get_os_path(path=path)
115 return os.path.isdir(os_path)
115 return os.path.isdir(os_path)
116
116
117 def is_hidden(self, path):
117 def is_hidden(self, path):
118 """Does the API style path correspond to a hidden directory or file?
118 """Does the API style path correspond to a hidden directory or file?
119
119
120 Parameters
120 Parameters
121 ----------
121 ----------
122 path : string
122 path : string
123 The path to check. This is an API path (`/` separated,
123 The path to check. This is an API path (`/` separated,
124 relative to base notebook-dir).
124 relative to base notebook-dir).
125
125
126 Returns
126 Returns
127 -------
127 -------
128 exists : bool
128 exists : bool
129 Whether the path is hidden.
129 Whether the path is hidden.
130
130
131 """
131 """
132 path = path.strip('/')
132 path = path.strip('/')
133 os_path = self._get_os_path(path=path)
133 os_path = self._get_os_path(path=path)
134 return is_hidden(os_path, self.notebook_dir)
134 return is_hidden(os_path, self.notebook_dir)
135
135
136 def _get_os_path(self, name=None, path=''):
136 def _get_os_path(self, name=None, path=''):
137 """Given a notebook name and a URL path, return its file system
137 """Given a notebook name and a URL path, return its file system
138 path.
138 path.
139
139
140 Parameters
140 Parameters
141 ----------
141 ----------
142 name : string
142 name : string
143 The name of a notebook file with the .ipynb extension
143 The name of a notebook file with the .ipynb extension
144 path : string
144 path : string
145 The relative URL path (with '/' as separator) to the named
145 The relative URL path (with '/' as separator) to the named
146 notebook.
146 notebook.
147
147
148 Returns
148 Returns
149 -------
149 -------
150 path : string
150 path : string
151 A file system path that combines notebook_dir (location where
151 A file system path that combines notebook_dir (location where
152 server started), the relative path, and the filename with the
152 server started), the relative path, and the filename with the
153 current operating system's url.
153 current operating system's url.
154 """
154 """
155 if name is not None:
155 if name is not None:
156 path = path + '/' + name
156 path = path + '/' + name
157 return to_os_path(path, self.notebook_dir)
157 return to_os_path(path, self.notebook_dir)
158
158
159 def notebook_exists(self, name, path=''):
159 def notebook_exists(self, name, path=''):
160 """Returns a True if the notebook exists. Else, returns False.
160 """Returns a True if the notebook exists. Else, returns False.
161
161
162 Parameters
162 Parameters
163 ----------
163 ----------
164 name : string
164 name : string
165 The name of the notebook you are checking.
165 The name of the notebook you are checking.
166 path : string
166 path : string
167 The relative path to the notebook (with '/' as separator)
167 The relative path to the notebook (with '/' as separator)
168
168
169 Returns
169 Returns
170 -------
170 -------
171 bool
171 bool
172 """
172 """
173 path = path.strip('/')
173 path = path.strip('/')
174 nbpath = self._get_os_path(name, path=path)
174 nbpath = self._get_os_path(name, path=path)
175 return os.path.isfile(nbpath)
175 return os.path.isfile(nbpath)
176
176
177 # TODO: Remove this after we create the contents web service and directories are
177 # TODO: Remove this after we create the contents web service and directories are
178 # no longer listed by the notebook web service.
178 # no longer listed by the notebook web service.
179 def list_dirs(self, path):
179 def list_dirs(self, path):
180 """List the directories for a given API style path."""
180 """List the directories for a given API style path."""
181 path = path.strip('/')
181 path = path.strip('/')
182 os_path = self._get_os_path('', path)
182 os_path = self._get_os_path('', path)
183 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
183 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
184 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
184 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
185 dir_names = os.listdir(os_path)
185 dir_names = os.listdir(os_path)
186 dirs = []
186 dirs = []
187 for name in dir_names:
187 for name in dir_names:
188 os_path = self._get_os_path(name, path)
188 os_path = self._get_os_path(name, path)
189 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
189 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
190 and not name.startswith('_'):
190 and self.should_list(name):
191 try:
191 try:
192 model = self.get_dir_model(name, path)
192 model = self.get_dir_model(name, path)
193 except IOError:
193 except IOError:
194 pass
194 pass
195 dirs.append(model)
195 dirs.append(model)
196 dirs = sorted(dirs, key=sort_key)
196 dirs = sorted(dirs, key=sort_key)
197 return dirs
197 return dirs
198
198
199 # TODO: Remove this after we create the contents web service and directories are
199 # TODO: Remove this after we create the contents web service and directories are
200 # no longer listed by the notebook web service.
200 # no longer listed by the notebook web service.
201 def get_dir_model(self, name, path=''):
201 def get_dir_model(self, name, path=''):
202 """Get the directory model given a directory name and its API style path"""
202 """Get the directory model given a directory name and its API style path"""
203 path = path.strip('/')
203 path = path.strip('/')
204 os_path = self._get_os_path(name, path)
204 os_path = self._get_os_path(name, path)
205 if not os.path.isdir(os_path):
205 if not os.path.isdir(os_path):
206 raise IOError('directory does not exist: %r' % os_path)
206 raise IOError('directory does not exist: %r' % os_path)
207 info = os.stat(os_path)
207 info = os.stat(os_path)
208 last_modified = tz.utcfromtimestamp(info.st_mtime)
208 last_modified = tz.utcfromtimestamp(info.st_mtime)
209 created = tz.utcfromtimestamp(info.st_ctime)
209 created = tz.utcfromtimestamp(info.st_ctime)
210 # Create the notebook model.
210 # Create the notebook model.
211 model ={}
211 model ={}
212 model['name'] = name
212 model['name'] = name
213 model['path'] = path
213 model['path'] = path
214 model['last_modified'] = last_modified
214 model['last_modified'] = last_modified
215 model['created'] = created
215 model['created'] = created
216 model['type'] = 'directory'
216 model['type'] = 'directory'
217 return model
217 return model
218
218
219 def list_notebooks(self, path):
219 def list_notebooks(self, path):
220 """Returns a list of dictionaries that are the standard model
220 """Returns a list of dictionaries that are the standard model
221 for all notebooks in the relative 'path'.
221 for all notebooks in the relative 'path'.
222
222
223 Parameters
223 Parameters
224 ----------
224 ----------
225 path : str
225 path : str
226 the URL path that describes the relative path for the
226 the URL path that describes the relative path for the
227 listed notebooks
227 listed notebooks
228
228
229 Returns
229 Returns
230 -------
230 -------
231 notebooks : list of dicts
231 notebooks : list of dicts
232 a list of the notebook models without 'content'
232 a list of the notebook models without 'content'
233 """
233 """
234 path = path.strip('/')
234 path = path.strip('/')
235 notebook_names = self.get_notebook_names(path)
235 notebook_names = self.get_notebook_names(path)
236 notebooks = [self.get_notebook(name, path, content=False) for name in notebook_names]
236 notebooks = [self.get_notebook(name, path, content=False)
237 for name in notebook_names if self.should_list(name)]
237 notebooks = sorted(notebooks, key=sort_key)
238 notebooks = sorted(notebooks, key=sort_key)
238 return notebooks
239 return notebooks
239
240
240 def get_notebook(self, name, path='', content=True):
241 def get_notebook(self, name, path='', content=True):
241 """ Takes a path and name for a notebook and returns its model
242 """ Takes a path and name for a notebook and returns its model
242
243
243 Parameters
244 Parameters
244 ----------
245 ----------
245 name : str
246 name : str
246 the name of the notebook
247 the name of the notebook
247 path : str
248 path : str
248 the URL path that describes the relative path for
249 the URL path that describes the relative path for
249 the notebook
250 the notebook
250
251
251 Returns
252 Returns
252 -------
253 -------
253 model : dict
254 model : dict
254 the notebook model. If contents=True, returns the 'contents'
255 the notebook model. If contents=True, returns the 'contents'
255 dict in the model as well.
256 dict in the model as well.
256 """
257 """
257 path = path.strip('/')
258 path = path.strip('/')
258 if not self.notebook_exists(name=name, path=path):
259 if not self.notebook_exists(name=name, path=path):
259 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
260 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
260 os_path = self._get_os_path(name, path)
261 os_path = self._get_os_path(name, path)
261 info = os.stat(os_path)
262 info = os.stat(os_path)
262 last_modified = tz.utcfromtimestamp(info.st_mtime)
263 last_modified = tz.utcfromtimestamp(info.st_mtime)
263 created = tz.utcfromtimestamp(info.st_ctime)
264 created = tz.utcfromtimestamp(info.st_ctime)
264 # Create the notebook model.
265 # Create the notebook model.
265 model ={}
266 model ={}
266 model['name'] = name
267 model['name'] = name
267 model['path'] = path
268 model['path'] = path
268 model['last_modified'] = last_modified
269 model['last_modified'] = last_modified
269 model['created'] = created
270 model['created'] = created
270 model['type'] = 'notebook'
271 model['type'] = 'notebook'
271 if content:
272 if content:
272 with io.open(os_path, 'r', encoding='utf-8') as f:
273 with io.open(os_path, 'r', encoding='utf-8') as f:
273 try:
274 try:
274 nb = current.read(f, u'json')
275 nb = current.read(f, u'json')
275 except Exception as e:
276 except Exception as e:
276 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
277 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
277 self.mark_trusted_cells(nb, path, name)
278 self.mark_trusted_cells(nb, path, name)
278 model['content'] = nb
279 model['content'] = nb
279 return model
280 return model
280
281
281 def save_notebook(self, model, name='', path=''):
282 def save_notebook(self, model, name='', path=''):
282 """Save the notebook model and return the model with no content."""
283 """Save the notebook model and return the model with no content."""
283 path = path.strip('/')
284 path = path.strip('/')
284
285
285 if 'content' not in model:
286 if 'content' not in model:
286 raise web.HTTPError(400, u'No notebook JSON data provided')
287 raise web.HTTPError(400, u'No notebook JSON data provided')
287
288
288 # One checkpoint should always exist
289 # One checkpoint should always exist
289 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
290 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
290 self.create_checkpoint(name, path)
291 self.create_checkpoint(name, path)
291
292
292 new_path = model.get('path', path).strip('/')
293 new_path = model.get('path', path).strip('/')
293 new_name = model.get('name', name)
294 new_name = model.get('name', name)
294
295
295 if path != new_path or name != new_name:
296 if path != new_path or name != new_name:
296 self.rename_notebook(name, path, new_name, new_path)
297 self.rename_notebook(name, path, new_name, new_path)
297
298
298 # Save the notebook file
299 # Save the notebook file
299 os_path = self._get_os_path(new_name, new_path)
300 os_path = self._get_os_path(new_name, new_path)
300 nb = current.to_notebook_json(model['content'])
301 nb = current.to_notebook_json(model['content'])
301
302
302 self.check_and_sign(nb, new_path, new_name)
303 self.check_and_sign(nb, new_path, new_name)
303
304
304 if 'name' in nb['metadata']:
305 if 'name' in nb['metadata']:
305 nb['metadata']['name'] = u''
306 nb['metadata']['name'] = u''
306 try:
307 try:
307 self.log.debug("Autosaving notebook %s", os_path)
308 self.log.debug("Autosaving notebook %s", os_path)
308 with io.open(os_path, 'w', encoding='utf-8') as f:
309 with io.open(os_path, 'w', encoding='utf-8') as f:
309 current.write(nb, f, u'json')
310 current.write(nb, f, u'json')
310 except Exception as e:
311 except Exception as e:
311 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
312 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
312
313
313 # Save .py script as well
314 # Save .py script as well
314 if self.save_script:
315 if self.save_script:
315 py_path = os.path.splitext(os_path)[0] + '.py'
316 py_path = os.path.splitext(os_path)[0] + '.py'
316 self.log.debug("Writing script %s", py_path)
317 self.log.debug("Writing script %s", py_path)
317 try:
318 try:
318 with io.open(py_path, 'w', encoding='utf-8') as f:
319 with io.open(py_path, 'w', encoding='utf-8') as f:
319 current.write(nb, f, u'py')
320 current.write(nb, f, u'py')
320 except Exception as e:
321 except Exception as e:
321 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
322 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
322
323
323 model = self.get_notebook(new_name, new_path, content=False)
324 model = self.get_notebook(new_name, new_path, content=False)
324 return model
325 return model
325
326
326 def update_notebook(self, model, name, path=''):
327 def update_notebook(self, model, name, path=''):
327 """Update the notebook's path and/or name"""
328 """Update the notebook's path and/or name"""
328 path = path.strip('/')
329 path = path.strip('/')
329 new_name = model.get('name', name)
330 new_name = model.get('name', name)
330 new_path = model.get('path', path).strip('/')
331 new_path = model.get('path', path).strip('/')
331 if path != new_path or name != new_name:
332 if path != new_path or name != new_name:
332 self.rename_notebook(name, path, new_name, new_path)
333 self.rename_notebook(name, path, new_name, new_path)
333 model = self.get_notebook(new_name, new_path, content=False)
334 model = self.get_notebook(new_name, new_path, content=False)
334 return model
335 return model
335
336
336 def delete_notebook(self, name, path=''):
337 def delete_notebook(self, name, path=''):
337 """Delete notebook by name and path."""
338 """Delete notebook by name and path."""
338 path = path.strip('/')
339 path = path.strip('/')
339 os_path = self._get_os_path(name, path)
340 os_path = self._get_os_path(name, path)
340 if not os.path.isfile(os_path):
341 if not os.path.isfile(os_path):
341 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
342 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
342
343
343 # clear checkpoints
344 # clear checkpoints
344 for checkpoint in self.list_checkpoints(name, path):
345 for checkpoint in self.list_checkpoints(name, path):
345 checkpoint_id = checkpoint['id']
346 checkpoint_id = checkpoint['id']
346 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
347 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
347 if os.path.isfile(cp_path):
348 if os.path.isfile(cp_path):
348 self.log.debug("Unlinking checkpoint %s", cp_path)
349 self.log.debug("Unlinking checkpoint %s", cp_path)
349 os.unlink(cp_path)
350 os.unlink(cp_path)
350
351
351 self.log.debug("Unlinking notebook %s", os_path)
352 self.log.debug("Unlinking notebook %s", os_path)
352 os.unlink(os_path)
353 os.unlink(os_path)
353
354
354 def rename_notebook(self, old_name, old_path, new_name, new_path):
355 def rename_notebook(self, old_name, old_path, new_name, new_path):
355 """Rename a notebook."""
356 """Rename a notebook."""
356 old_path = old_path.strip('/')
357 old_path = old_path.strip('/')
357 new_path = new_path.strip('/')
358 new_path = new_path.strip('/')
358 if new_name == old_name and new_path == old_path:
359 if new_name == old_name and new_path == old_path:
359 return
360 return
360
361
361 new_os_path = self._get_os_path(new_name, new_path)
362 new_os_path = self._get_os_path(new_name, new_path)
362 old_os_path = self._get_os_path(old_name, old_path)
363 old_os_path = self._get_os_path(old_name, old_path)
363
364
364 # Should we proceed with the move?
365 # Should we proceed with the move?
365 if os.path.isfile(new_os_path):
366 if os.path.isfile(new_os_path):
366 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
367 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
367 if self.save_script:
368 if self.save_script:
368 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
369 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
369 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
370 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
370 if os.path.isfile(new_py_path):
371 if os.path.isfile(new_py_path):
371 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
372 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
372
373
373 # Move the notebook file
374 # Move the notebook file
374 try:
375 try:
375 os.rename(old_os_path, new_os_path)
376 os.rename(old_os_path, new_os_path)
376 except Exception as e:
377 except Exception as e:
377 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
378 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
378
379
379 # Move the checkpoints
380 # Move the checkpoints
380 old_checkpoints = self.list_checkpoints(old_name, old_path)
381 old_checkpoints = self.list_checkpoints(old_name, old_path)
381 for cp in old_checkpoints:
382 for cp in old_checkpoints:
382 checkpoint_id = cp['id']
383 checkpoint_id = cp['id']
383 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
384 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
384 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
385 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
385 if os.path.isfile(old_cp_path):
386 if os.path.isfile(old_cp_path):
386 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
387 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
387 os.rename(old_cp_path, new_cp_path)
388 os.rename(old_cp_path, new_cp_path)
388
389
389 # Move the .py script
390 # Move the .py script
390 if self.save_script:
391 if self.save_script:
391 os.rename(old_py_path, new_py_path)
392 os.rename(old_py_path, new_py_path)
392
393
393 # Checkpoint-related utilities
394 # Checkpoint-related utilities
394
395
395 def get_checkpoint_path(self, checkpoint_id, name, path=''):
396 def get_checkpoint_path(self, checkpoint_id, name, path=''):
396 """find the path to a checkpoint"""
397 """find the path to a checkpoint"""
397 path = path.strip('/')
398 path = path.strip('/')
398 basename, _ = os.path.splitext(name)
399 basename, _ = os.path.splitext(name)
399 filename = u"{name}-{checkpoint_id}{ext}".format(
400 filename = u"{name}-{checkpoint_id}{ext}".format(
400 name=basename,
401 name=basename,
401 checkpoint_id=checkpoint_id,
402 checkpoint_id=checkpoint_id,
402 ext=self.filename_ext,
403 ext=self.filename_ext,
403 )
404 )
404 cp_path = os.path.join(path, self.checkpoint_dir, filename)
405 cp_path = os.path.join(path, self.checkpoint_dir, filename)
405 return cp_path
406 return cp_path
406
407
407 def get_checkpoint_model(self, checkpoint_id, name, path=''):
408 def get_checkpoint_model(self, checkpoint_id, name, path=''):
408 """construct the info dict for a given checkpoint"""
409 """construct the info dict for a given checkpoint"""
409 path = path.strip('/')
410 path = path.strip('/')
410 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
411 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
411 stats = os.stat(cp_path)
412 stats = os.stat(cp_path)
412 last_modified = tz.utcfromtimestamp(stats.st_mtime)
413 last_modified = tz.utcfromtimestamp(stats.st_mtime)
413 info = dict(
414 info = dict(
414 id = checkpoint_id,
415 id = checkpoint_id,
415 last_modified = last_modified,
416 last_modified = last_modified,
416 )
417 )
417 return info
418 return info
418
419
419 # public checkpoint API
420 # public checkpoint API
420
421
421 def create_checkpoint(self, name, path=''):
422 def create_checkpoint(self, name, path=''):
422 """Create a checkpoint from the current state of a notebook"""
423 """Create a checkpoint from the current state of a notebook"""
423 path = path.strip('/')
424 path = path.strip('/')
424 nb_path = self._get_os_path(name, path)
425 nb_path = self._get_os_path(name, path)
425 # only the one checkpoint ID:
426 # only the one checkpoint ID:
426 checkpoint_id = u"checkpoint"
427 checkpoint_id = u"checkpoint"
427 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
428 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
428 self.log.debug("creating checkpoint for notebook %s", name)
429 self.log.debug("creating checkpoint for notebook %s", name)
429 if not os.path.exists(self.checkpoint_dir):
430 if not os.path.exists(self.checkpoint_dir):
430 os.mkdir(self.checkpoint_dir)
431 os.mkdir(self.checkpoint_dir)
431 shutil.copy2(nb_path, cp_path)
432 shutil.copy2(nb_path, cp_path)
432
433
433 # return the checkpoint info
434 # return the checkpoint info
434 return self.get_checkpoint_model(checkpoint_id, name, path)
435 return self.get_checkpoint_model(checkpoint_id, name, path)
435
436
436 def list_checkpoints(self, name, path=''):
437 def list_checkpoints(self, name, path=''):
437 """list the checkpoints for a given notebook
438 """list the checkpoints for a given notebook
438
439
439 This notebook manager currently only supports one checkpoint per notebook.
440 This notebook manager currently only supports one checkpoint per notebook.
440 """
441 """
441 path = path.strip('/')
442 path = path.strip('/')
442 checkpoint_id = "checkpoint"
443 checkpoint_id = "checkpoint"
443 path = self.get_checkpoint_path(checkpoint_id, name, path)
444 path = self.get_checkpoint_path(checkpoint_id, name, path)
444 if not os.path.exists(path):
445 if not os.path.exists(path):
445 return []
446 return []
446 else:
447 else:
447 return [self.get_checkpoint_model(checkpoint_id, name, path)]
448 return [self.get_checkpoint_model(checkpoint_id, name, path)]
448
449
449
450
450 def restore_checkpoint(self, checkpoint_id, name, path=''):
451 def restore_checkpoint(self, checkpoint_id, name, path=''):
451 """restore a notebook to a checkpointed state"""
452 """restore a notebook to a checkpointed state"""
452 path = path.strip('/')
453 path = path.strip('/')
453 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
454 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
454 nb_path = self._get_os_path(name, path)
455 nb_path = self._get_os_path(name, path)
455 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
456 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
456 if not os.path.isfile(cp_path):
457 if not os.path.isfile(cp_path):
457 self.log.debug("checkpoint file does not exist: %s", cp_path)
458 self.log.debug("checkpoint file does not exist: %s", cp_path)
458 raise web.HTTPError(404,
459 raise web.HTTPError(404,
459 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
460 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
460 )
461 )
461 # ensure notebook is readable (never restore from an unreadable notebook)
462 # ensure notebook is readable (never restore from an unreadable notebook)
462 with io.open(cp_path, 'r', encoding='utf-8') as f:
463 with io.open(cp_path, 'r', encoding='utf-8') as f:
463 nb = current.read(f, u'json')
464 nb = current.read(f, u'json')
464 shutil.copy2(cp_path, nb_path)
465 shutil.copy2(cp_path, nb_path)
465 self.log.debug("copying %s -> %s", cp_path, nb_path)
466 self.log.debug("copying %s -> %s", cp_path, nb_path)
466
467
467 def delete_checkpoint(self, checkpoint_id, name, path=''):
468 def delete_checkpoint(self, checkpoint_id, name, path=''):
468 """delete a notebook's checkpoint"""
469 """delete a notebook's checkpoint"""
469 path = path.strip('/')
470 path = path.strip('/')
470 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
471 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
471 if not os.path.isfile(cp_path):
472 if not os.path.isfile(cp_path):
472 raise web.HTTPError(404,
473 raise web.HTTPError(404,
473 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
474 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
474 )
475 )
475 self.log.debug("unlinking %s", cp_path)
476 self.log.debug("unlinking %s", cp_path)
476 os.unlink(cp_path)
477 os.unlink(cp_path)
477
478
478 def info_string(self):
479 def info_string(self):
479 return "Serving notebooks from local directory: %s" % self.notebook_dir
480 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,243 +1,251 b''
1 """A base class notebook manager.
1 """A base class notebook manager.
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 from fnmatch import fnmatch
20 import itertools
21 import itertools
21 import os
22 import os
22
23
23 from IPython.config.configurable import LoggingConfigurable
24 from IPython.config.configurable import LoggingConfigurable
24 from IPython.nbformat import current, sign
25 from IPython.nbformat import current, sign
25 from IPython.utils.traitlets import Instance, Unicode
26 from IPython.utils.traitlets import Instance, Unicode, List
26
27
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28 # Classes
29 # Classes
29 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
30
31
31 class NotebookManager(LoggingConfigurable):
32 class NotebookManager(LoggingConfigurable):
32
33
33 filename_ext = Unicode(u'.ipynb')
34 filename_ext = Unicode(u'.ipynb')
34
35
35 notary = Instance(sign.NotebookNotary)
36 notary = Instance(sign.NotebookNotary)
36 def _notary_default(self):
37 def _notary_default(self):
37 return sign.NotebookNotary(parent=self)
38 return sign.NotebookNotary(parent=self)
38
39
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 Glob patterns to hide in file and directory listings.
42 """)
43
39 # NotebookManager API part 1: methods that must be
44 # NotebookManager API part 1: methods that must be
40 # implemented in subclasses.
45 # implemented in subclasses.
41
46
42 def path_exists(self, path):
47 def path_exists(self, path):
43 """Does the API-style path (directory) actually exist?
48 """Does the API-style path (directory) actually exist?
44
49
45 Override this method in subclasses.
50 Override this method in subclasses.
46
51
47 Parameters
52 Parameters
48 ----------
53 ----------
49 path : string
54 path : string
50 The
55 The
51
56
52 Returns
57 Returns
53 -------
58 -------
54 exists : bool
59 exists : bool
55 Whether the path does indeed exist.
60 Whether the path does indeed exist.
56 """
61 """
57 raise NotImplementedError
62 raise NotImplementedError
58
63
59 def is_hidden(self, path):
64 def is_hidden(self, path):
60 """Does the API style path correspond to a hidden directory or file?
65 """Does the API style path correspond to a hidden directory or file?
61
66
62 Parameters
67 Parameters
63 ----------
68 ----------
64 path : string
69 path : string
65 The path to check. This is an API path (`/` separated,
70 The path to check. This is an API path (`/` separated,
66 relative to base notebook-dir).
71 relative to base notebook-dir).
67
72
68 Returns
73 Returns
69 -------
74 -------
70 exists : bool
75 exists : bool
71 Whether the path is hidden.
76 Whether the path is hidden.
72
77
73 """
78 """
74 raise NotImplementedError
79 raise NotImplementedError
75
80
76 def notebook_exists(self, name, path=''):
81 def notebook_exists(self, name, path=''):
77 """Returns a True if the notebook exists. Else, returns False.
82 """Returns a True if the notebook exists. Else, returns False.
78
83
79 Parameters
84 Parameters
80 ----------
85 ----------
81 name : string
86 name : string
82 The name of the notebook you are checking.
87 The name of the notebook you are checking.
83 path : string
88 path : string
84 The relative path to the notebook (with '/' as separator)
89 The relative path to the notebook (with '/' as separator)
85
90
86 Returns
91 Returns
87 -------
92 -------
88 bool
93 bool
89 """
94 """
90 raise NotImplementedError('must be implemented in a subclass')
95 raise NotImplementedError('must be implemented in a subclass')
91
96
92 # TODO: Remove this after we create the contents web service and directories are
97 # TODO: Remove this after we create the contents web service and directories are
93 # no longer listed by the notebook web service.
98 # no longer listed by the notebook web service.
94 def list_dirs(self, path):
99 def list_dirs(self, path):
95 """List the directory models for a given API style path."""
100 """List the directory models for a given API style path."""
96 raise NotImplementedError('must be implemented in a subclass')
101 raise NotImplementedError('must be implemented in a subclass')
97
102
98 # TODO: Remove this after we create the contents web service and directories are
103 # TODO: Remove this after we create the contents web service and directories are
99 # no longer listed by the notebook web service.
104 # no longer listed by the notebook web service.
100 def get_dir_model(self, name, path=''):
105 def get_dir_model(self, name, path=''):
101 """Get the directory model given a directory name and its API style path.
106 """Get the directory model given a directory name and its API style path.
102
107
103 The keys in the model should be:
108 The keys in the model should be:
104 * name
109 * name
105 * path
110 * path
106 * last_modified
111 * last_modified
107 * created
112 * created
108 * type='directory'
113 * type='directory'
109 """
114 """
110 raise NotImplementedError('must be implemented in a subclass')
115 raise NotImplementedError('must be implemented in a subclass')
111
116
112 def list_notebooks(self, path=''):
117 def list_notebooks(self, path=''):
113 """Return a list of notebook dicts without content.
118 """Return a list of notebook dicts without content.
114
119
115 This returns a list of dicts, each of the form::
120 This returns a list of dicts, each of the form::
116
121
117 dict(notebook_id=notebook,name=name)
122 dict(notebook_id=notebook,name=name)
118
123
119 This list of dicts should be sorted by name::
124 This list of dicts should be sorted by name::
120
125
121 data = sorted(data, key=lambda item: item['name'])
126 data = sorted(data, key=lambda item: item['name'])
122 """
127 """
123 raise NotImplementedError('must be implemented in a subclass')
128 raise NotImplementedError('must be implemented in a subclass')
124
129
125 def get_notebook(self, name, path='', content=True):
130 def get_notebook(self, name, path='', content=True):
126 """Get the notebook model with or without content."""
131 """Get the notebook model with or without content."""
127 raise NotImplementedError('must be implemented in a subclass')
132 raise NotImplementedError('must be implemented in a subclass')
128
133
129 def save_notebook(self, model, name, path=''):
134 def save_notebook(self, model, name, path=''):
130 """Save the notebook and return the model with no content."""
135 """Save the notebook and return the model with no content."""
131 raise NotImplementedError('must be implemented in a subclass')
136 raise NotImplementedError('must be implemented in a subclass')
132
137
133 def update_notebook(self, model, name, path=''):
138 def update_notebook(self, model, name, path=''):
134 """Update the notebook and return the model with no content."""
139 """Update the notebook and return the model with no content."""
135 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
136
141
137 def delete_notebook(self, name, path=''):
142 def delete_notebook(self, name, path=''):
138 """Delete notebook by name and path."""
143 """Delete notebook by name and path."""
139 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
140
145
141 def create_checkpoint(self, name, path=''):
146 def create_checkpoint(self, name, path=''):
142 """Create a checkpoint of the current state of a notebook
147 """Create a checkpoint of the current state of a notebook
143
148
144 Returns a checkpoint_id for the new checkpoint.
149 Returns a checkpoint_id for the new checkpoint.
145 """
150 """
146 raise NotImplementedError("must be implemented in a subclass")
151 raise NotImplementedError("must be implemented in a subclass")
147
152
148 def list_checkpoints(self, name, path=''):
153 def list_checkpoints(self, name, path=''):
149 """Return a list of checkpoints for a given notebook"""
154 """Return a list of checkpoints for a given notebook"""
150 return []
155 return []
151
156
152 def restore_checkpoint(self, checkpoint_id, name, path=''):
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
153 """Restore a notebook from one of its checkpoints"""
158 """Restore a notebook from one of its checkpoints"""
154 raise NotImplementedError("must be implemented in a subclass")
159 raise NotImplementedError("must be implemented in a subclass")
155
160
156 def delete_checkpoint(self, checkpoint_id, name, path=''):
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
157 """delete a checkpoint for a notebook"""
162 """delete a checkpoint for a notebook"""
158 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
159
164
160 def info_string(self):
165 def info_string(self):
161 return "Serving notebooks"
166 return "Serving notebooks"
162
167
163 # NotebookManager API part 2: methods that have useable default
168 # NotebookManager API part 2: methods that have useable default
164 # implementations, but can be overridden in subclasses.
169 # implementations, but can be overridden in subclasses.
165
170
166 def increment_filename(self, basename, path=''):
171 def increment_filename(self, basename, path=''):
167 """Increment a notebook filename without the .ipynb to make it unique.
172 """Increment a notebook filename without the .ipynb to make it unique.
168
173
169 Parameters
174 Parameters
170 ----------
175 ----------
171 basename : unicode
176 basename : unicode
172 The name of a notebook without the ``.ipynb`` file extension.
177 The name of a notebook without the ``.ipynb`` file extension.
173 path : unicode
178 path : unicode
174 The URL path of the notebooks directory
179 The URL path of the notebooks directory
175
180
176 Returns
181 Returns
177 -------
182 -------
178 name : unicode
183 name : unicode
179 A notebook name (with the .ipynb extension) that starts
184 A notebook name (with the .ipynb extension) that starts
180 with basename and does not refer to any existing notebook.
185 with basename and does not refer to any existing notebook.
181 """
186 """
182 path = path.strip('/')
187 path = path.strip('/')
183 for i in itertools.count():
188 for i in itertools.count():
184 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
189 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
185 ext=self.filename_ext)
190 ext=self.filename_ext)
186 if not self.notebook_exists(name, path):
191 if not self.notebook_exists(name, path):
187 break
192 break
188 return name
193 return name
189
194
190 def create_notebook(self, model=None, path=''):
195 def create_notebook(self, model=None, path=''):
191 """Create a new notebook and return its model with no content."""
196 """Create a new notebook and return its model with no content."""
192 path = path.strip('/')
197 path = path.strip('/')
193 if model is None:
198 if model is None:
194 model = {}
199 model = {}
195 if 'content' not in model:
200 if 'content' not in model:
196 metadata = current.new_metadata(name=u'')
201 metadata = current.new_metadata(name=u'')
197 model['content'] = current.new_notebook(metadata=metadata)
202 model['content'] = current.new_notebook(metadata=metadata)
198 if 'name' not in model:
203 if 'name' not in model:
199 model['name'] = self.increment_filename('Untitled', path)
204 model['name'] = self.increment_filename('Untitled', path)
200
205
201 model['path'] = path
206 model['path'] = path
202 model = self.save_notebook(model, model['name'], model['path'])
207 model = self.save_notebook(model, model['name'], model['path'])
203 return model
208 return model
204
209
205 def copy_notebook(self, from_name, to_name=None, path=''):
210 def copy_notebook(self, from_name, to_name=None, path=''):
206 """Copy an existing notebook and return its new model.
211 """Copy an existing notebook and return its new model.
207
212
208 If to_name not specified, increment `from_name-Copy#.ipynb`.
213 If to_name not specified, increment `from_name-Copy#.ipynb`.
209 """
214 """
210 path = path.strip('/')
215 path = path.strip('/')
211 model = self.get_notebook(from_name, path)
216 model = self.get_notebook(from_name, path)
212 if not to_name:
217 if not to_name:
213 base = os.path.splitext(from_name)[0] + '-Copy'
218 base = os.path.splitext(from_name)[0] + '-Copy'
214 to_name = self.increment_filename(base, path)
219 to_name = self.increment_filename(base, path)
215 model['name'] = to_name
220 model['name'] = to_name
216 model = self.save_notebook(model, to_name, path)
221 model = self.save_notebook(model, to_name, path)
217 return model
222 return model
218
223
219 def log_info(self):
224 def log_info(self):
220 self.log.info(self.info_string())
225 self.log.info(self.info_string())
221
226
222 # NotebookManager methods provided for use in subclasses.
227 # NotebookManager methods provided for use in subclasses.
223
228
224 def check_and_sign(self, nb, path, name):
229 def check_and_sign(self, nb, path, name):
225 """Check for trusted cells, and sign the notebook.
230 """Check for trusted cells, and sign the notebook.
226
231
227 Called as a part of saving notebooks.
232 Called as a part of saving notebooks.
228 """
233 """
229 if self.notary.check_cells(nb):
234 if self.notary.check_cells(nb):
230 self.notary.sign(nb)
235 self.notary.sign(nb)
231 else:
236 else:
232 self.log.warn("Saving untrusted notebook %s/%s", path, name)
237 self.log.warn("Saving untrusted notebook %s/%s", path, name)
233
238
234 def mark_trusted_cells(self, nb, path, name):
239 def mark_trusted_cells(self, nb, path, name):
235 """Mark cells as trusted if the notebook signature matches.
240 """Mark cells as trusted if the notebook signature matches.
236
241
237 Called as a part of loading notebooks.
242 Called as a part of loading notebooks.
238 """
243 """
239 trusted = self.notary.check_signature(nb)
244 trusted = self.notary.check_signature(nb)
240 if not trusted:
245 if not trusted:
241 self.log.warn("Notebook %s/%s is not trusted", path, name)
246 self.log.warn("Notebook %s/%s is not trusted", path, name)
242 self.notary.mark_cells(nb, trusted)
247 self.notary.mark_cells(nb, trusted)
243
248
249 def should_list(self, name):
250 """Should this file/directory name be displayed in a listing?"""
251 return not any(fnmatch(name, glob) for glob in self.hide_globs)
General Comments 0
You need to be logged in to leave comments. Login now