##// END OF EJS Templates
ensure 'path' never has leading or trailing slash in nbmanager...
MinRK -
Show More
@@ -1,395 +1,412 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import datetime
21 21 import io
22 22 import os
23 23 import glob
24 24 import shutil
25 25
26 26 from unicodedata import normalize
27 27
28 28 from tornado import web
29 29
30 30 from .nbmanager import NotebookManager
31 31 from IPython.nbformat import current
32 32 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
33 33 from IPython.utils import tz
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class FileNotebookManager(NotebookManager):
40 40
41 41 save_script = Bool(False, config=True,
42 42 help="""Automatically create a Python script when saving the notebook.
43 43
44 44 For easier use of import, %run and %load across notebooks, a
45 45 <notebook-name>.py script will be created next to any
46 46 <notebook-name>.ipynb on each save. This can also be set with the
47 47 short `--script` flag.
48 48 """
49 49 )
50 50
51 51 checkpoint_dir = Unicode(config=True,
52 52 help="""The location in which to keep notebook checkpoints
53 53
54 54 By default, it is notebook-dir/.ipynb_checkpoints
55 55 """
56 56 )
57 57 def _checkpoint_dir_default(self):
58 58 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
59 59
60 60 def _checkpoint_dir_changed(self, name, old, new):
61 61 """do a bit of validation of the checkpoint dir"""
62 62 if not os.path.isabs(new):
63 63 # If we receive a non-absolute path, make it absolute.
64 64 abs_new = os.path.abspath(new)
65 65 self.checkpoint_dir = abs_new
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("checkpoint dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating checkpoint dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create checkpoint dir %r" % new)
75 75
76 76 def get_notebook_names(self, path=''):
77 77 """List all notebook names in the notebook dir and path."""
78 path = path.strip('/')
78 79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
79 80 names = [os.path.basename(name)
80 81 for name in names]
81 82 return names
82 83
83 84 def increment_filename(self, basename, path=''):
84 85 """Return a non-used filename of the form basename<int>."""
86 path = path.strip('/')
85 87 i = 0
86 88 while True:
87 89 name = u'%s%i.ipynb' % (basename,i)
88 90 os_path = self.get_os_path(name, path)
89 91 if not os.path.isfile(os_path):
90 92 break
91 93 else:
92 94 i = i+1
93 95 return name
94 96
95 97 def path_exists(self, path):
96 98 """Does the API-style path (directory) actually exist?
97 99
98 100 Parameters
99 101 ----------
100 102 path : string
101 103 The path to check. This is an API path (`/` separated,
102 104 relative to base notebook-dir).
103 105
104 106 Returns
105 107 -------
106 108 exists : bool
107 109 Whether the path is indeed a directory.
108 110 """
111 path = path.strip('/')
109 112 os_path = self.get_os_path(path=path)
110 113 return os.path.isdir(os_path)
111 114
112 115 def get_os_path(self, name=None, path=''):
113 116 """Given a notebook name and a URL path, return its file system
114 117 path.
115 118
116 119 Parameters
117 120 ----------
118 121 name : string
119 122 The name of a notebook file with the .ipynb extension
120 123 path : string
121 124 The relative URL path (with '/' as separator) to the named
122 125 notebook.
123 126
124 127 Returns
125 128 -------
126 129 path : string
127 130 A file system path that combines notebook_dir (location where
128 131 server started), the relative path, and the filename with the
129 132 current operating system's url.
130 133 """
131 134 parts = path.strip('/').split('/')
132 135 parts = [p for p in parts if p != ''] # remove duplicate splits
133 136 if name is not None:
134 137 parts.append(name)
135 138 path = os.path.join(self.notebook_dir, *parts)
136 139 return path
137 140
138 141 def notebook_exists(self, name, path=''):
139 142 """Returns a True if the notebook exists. Else, returns False.
140 143
141 144 Parameters
142 145 ----------
143 146 name : string
144 147 The name of the notebook you are checking.
145 148 path : string
146 149 The relative path to the notebook (with '/' as separator)
147 150
148 151 Returns
149 152 -------
150 153 bool
151 154 """
155 path = path.strip('/')
152 156 nbpath = self.get_os_path(name, path=path)
153 157 return os.path.isfile(nbpath)
154 158
155 159 def list_notebooks(self, path):
156 160 """Returns a list of dictionaries that are the standard model
157 161 for all notebooks in the relative 'path'.
158 162
159 163 Parameters
160 164 ----------
161 165 path : str
162 166 the URL path that describes the relative path for the
163 167 listed notebooks
164 168
165 169 Returns
166 170 -------
167 171 notebooks : list of dicts
168 172 a list of the notebook models without 'content'
169 173 """
174 path = path.strip('/')
170 175 notebook_names = self.get_notebook_names(path)
171 176 notebooks = []
172 177 for name in notebook_names:
173 178 model = self.get_notebook_model(name, path, content=False)
174 179 notebooks.append(model)
175 180 notebooks = sorted(notebooks, key=lambda item: item['name'])
176 181 return notebooks
177 182
178 183 def get_notebook_model(self, name, path='', content=True):
179 184 """ Takes a path and name for a notebook and returns it's model
180 185
181 186 Parameters
182 187 ----------
183 188 name : str
184 189 the name of the notebook
185 190 path : str
186 191 the URL path that describes the relative path for
187 192 the notebook
188 193
189 194 Returns
190 195 -------
191 196 model : dict
192 197 the notebook model. If contents=True, returns the 'contents'
193 198 dict in the model as well.
194 199 """
200 path = path.strip('/')
195 201 if not self.notebook_exists(name=name, path=path):
196 202 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
197 203 os_path = self.get_os_path(name, path)
198 204 info = os.stat(os_path)
199 205 last_modified = tz.utcfromtimestamp(info.st_mtime)
200 206 created = tz.utcfromtimestamp(info.st_ctime)
201 207 # Create the notebook model.
202 208 model ={}
203 209 model['name'] = name
204 210 model['path'] = path
205 211 model['last_modified'] = last_modified
206 212 model['created'] = last_modified
207 213 if content is True:
208 214 with open(os_path, 'r') as f:
209 215 try:
210 216 nb = current.read(f, u'json')
211 217 except Exception as e:
212 218 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
213 219 model['content'] = nb
214 220 return model
215 221
216 222 def save_notebook_model(self, model, name='', path=''):
217 223 """Save the notebook model and return the model with no content."""
224 path = path.strip('/')
218 225
219 226 if 'content' not in model:
220 227 raise web.HTTPError(400, u'No notebook JSON data provided')
221 228
222 229 new_path = model.get('path', path).strip('/')
223 230 new_name = model.get('name', name)
224 231
225 232 if path != new_path or name != new_name:
226 233 self.rename_notebook(name, path, new_name, new_path)
227 234
228 235 # Save the notebook file
229 236 os_path = self.get_os_path(new_name, new_path)
230 237 nb = current.to_notebook_json(model['content'])
231 238 if 'name' in nb['metadata']:
232 239 nb['metadata']['name'] = u''
233 240 try:
234 241 self.log.debug("Autosaving notebook %s", os_path)
235 242 with open(os_path, 'w') as f:
236 243 current.write(nb, f, u'json')
237 244 except Exception as e:
238 245 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
239 246
240 247 # Save .py script as well
241 248 if self.save_script:
242 249 py_path = os.path.splitext(os_path)[0] + '.py'
243 250 self.log.debug("Writing script %s", py_path)
244 251 try:
245 252 with io.open(py_path, 'w', encoding='utf-8') as f:
246 253 current.write(model, f, u'py')
247 254 except Exception as e:
248 255 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
249 256
250 257 model = self.get_notebook_model(name, path, content=False)
251 258 return model
252 259
253 def update_notebook_model(self, model, name, path='/'):
260 def update_notebook_model(self, model, name, path=''):
254 261 """Update the notebook's path and/or name"""
262 path = path.strip('/')
255 263 new_name = model.get('name', name)
256 new_path = model.get('path', path)
264 new_path = model.get('path', path).strip('/')
257 265 if path != new_path or name != new_name:
258 266 self.rename_notebook(name, path, new_name, new_path)
259 267 model = self.get_notebook_model(new_name, new_path, content=False)
260 268 return model
261 269
262 def delete_notebook_model(self, name, path='/'):
270 def delete_notebook_model(self, name, path=''):
263 271 """Delete notebook by name and path."""
272 path = path.strip('/')
264 273 os_path = self.get_os_path(name, path)
265 274 if not os.path.isfile(os_path):
266 275 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
267 276
268 277 # clear checkpoints
269 278 for checkpoint in self.list_checkpoints(name, path):
270 279 checkpoint_id = checkpoint['checkpoint_id']
271 280 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
272 281 if os.path.isfile(cp_path):
273 282 self.log.debug("Unlinking checkpoint %s", cp_path)
274 283 os.unlink(cp_path)
275 284
276 285 self.log.debug("Unlinking notebook %s", os_path)
277 286 os.unlink(os_path)
278 287
279 288 def rename_notebook(self, old_name, old_path, new_name, new_path):
280 289 """Rename a notebook."""
290 old_path = old_path.strip('/')
291 new_path = new_path.strip('/')
281 292 if new_name == old_name and new_path == old_path:
282 293 return
283 294
284 295 new_os_path = self.get_os_path(new_name, new_path)
285 296 old_os_path = self.get_os_path(old_name, old_path)
286 297
287 298 # Should we proceed with the move?
288 299 if os.path.isfile(new_os_path):
289 300 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
290 301 if self.save_script:
291 302 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
292 303 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
293 304 if os.path.isfile(new_py_path):
294 305 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
295 306
296 307 # Move the notebook file
297 308 try:
298 309 os.rename(old_os_path, new_os_path)
299 310 except Exception as e:
300 311 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
301 312
302 313 # Move the checkpoints
303 314 old_checkpoints = self.list_checkpoints(old_name, old_path)
304 315 for cp in old_checkpoints:
305 316 checkpoint_id = cp['checkpoint_id']
306 317 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
307 318 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
308 319 if os.path.isfile(old_cp_path):
309 320 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
310 321 os.rename(old_cp_path, new_cp_path)
311 322
312 323 # Move the .py script
313 324 if self.save_script:
314 325 os.rename(old_py_path, new_py_path)
315 326
316 327 # Checkpoint-related utilities
317 328
318 def get_checkpoint_path(self, checkpoint_id, name, path='/'):
329 def get_checkpoint_path(self, checkpoint_id, name, path=''):
319 330 """find the path to a checkpoint"""
331 path = path.strip('/')
320 332 filename = u"{name}-{checkpoint_id}{ext}".format(
321 333 name=name,
322 334 checkpoint_id=checkpoint_id,
323 335 ext=self.filename_ext,
324 336 )
325 337 cp_path = os.path.join(path, self.checkpoint_dir, filename)
326 338 return cp_path
327 339
328 def get_checkpoint_model(self, checkpoint_id, name, path='/'):
340 def get_checkpoint_model(self, checkpoint_id, name, path=''):
329 341 """construct the info dict for a given checkpoint"""
342 path = path.strip('/')
330 343 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
331 344 stats = os.stat(cp_path)
332 345 last_modified = tz.utcfromtimestamp(stats.st_mtime)
333 346 info = dict(
334 347 checkpoint_id = checkpoint_id,
335 348 last_modified = last_modified,
336 349 )
337 350 return info
338 351
339 352 # public checkpoint API
340 353
341 def create_checkpoint(self, name, path='/'):
354 def create_checkpoint(self, name, path=''):
342 355 """Create a checkpoint from the current state of a notebook"""
356 path = path.strip('/')
343 357 nb_path = self.get_os_path(name, path)
344 358 # only the one checkpoint ID:
345 359 checkpoint_id = u"checkpoint"
346 360 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
347 361 self.log.debug("creating checkpoint for notebook %s", name)
348 362 if not os.path.exists(self.checkpoint_dir):
349 363 os.mkdir(self.checkpoint_dir)
350 364 shutil.copy2(nb_path, cp_path)
351 365
352 366 # return the checkpoint info
353 367 return self.get_checkpoint_model(checkpoint_id, name, path)
354 368
355 def list_checkpoints(self, name, path='/'):
369 def list_checkpoints(self, name, path=''):
356 370 """list the checkpoints for a given notebook
357 371
358 372 This notebook manager currently only supports one checkpoint per notebook.
359 373 """
374 path = path.strip('/')
360 375 checkpoint_id = "checkpoint"
361 376 path = self.get_checkpoint_path(checkpoint_id, name, path)
362 377 if not os.path.exists(path):
363 378 return []
364 379 else:
365 380 return [self.get_checkpoint_model(checkpoint_id, name, path)]
366 381
367 382
368 def restore_checkpoint(self, checkpoint_id, name, path='/'):
383 def restore_checkpoint(self, checkpoint_id, name, path=''):
369 384 """restore a notebook to a checkpointed state"""
385 path = path.strip('/')
370 386 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
371 387 nb_path = self.get_os_path(name, path)
372 388 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
373 389 if not os.path.isfile(cp_path):
374 390 self.log.debug("checkpoint file does not exist: %s", cp_path)
375 391 raise web.HTTPError(404,
376 392 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
377 393 )
378 394 # ensure notebook is readable (never restore from an unreadable notebook)
379 395 with file(cp_path, 'r') as f:
380 396 nb = current.read(f, u'json')
381 397 shutil.copy2(cp_path, nb_path)
382 398 self.log.debug("copying %s -> %s", cp_path, nb_path)
383 399
384 def delete_checkpoint(self, checkpoint_id, name, path='/'):
400 def delete_checkpoint(self, checkpoint_id, name, path=''):
385 401 """delete a notebook's checkpoint"""
402 path = path.strip('/')
386 403 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
387 404 if not os.path.isfile(cp_path):
388 405 raise web.HTTPError(404,
389 406 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
390 407 )
391 408 self.log.debug("unlinking %s", cp_path)
392 409 os.unlink(cp_path)
393 410
394 411 def info_string(self):
395 412 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,167 +1,169 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 * Zach Sailer
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2011 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 import os
21 21
22 22 from IPython.config.configurable import LoggingConfigurable
23 23 from IPython.nbformat import current
24 24 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29
30 30 class NotebookManager(LoggingConfigurable):
31 31
32 32 # Todo:
33 33 # The notebook_dir attribute is used to mean a couple of different things:
34 34 # 1. Where the notebooks are stored if FileNotebookManager is used.
35 35 # 2. The cwd of the kernel for a project.
36 36 # Right now we use this attribute in a number of different places and
37 37 # we are going to have to disentangle all of this.
38 38 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
39 39 The directory to use for notebooks.
40 40 """)
41 41
42 42 filename_ext = Unicode(u'.ipynb')
43 43
44 44 def path_exists(self, path):
45 45 """Does the API-style path (directory) actually exist?
46 46
47 47 Override this method in subclasses.
48 48
49 49 Parameters
50 50 ----------
51 51 path : string
52 52 The
53 53
54 54 Returns
55 55 -------
56 56 exists : bool
57 57 Whether the path does indeed exist.
58 58 """
59 59 raise NotImplementedError
60 60
61 61 def _notebook_dir_changed(self, name, old, new):
62 62 """Do a bit of validation of the notebook dir."""
63 63 if not os.path.isabs(new):
64 64 # If we receive a non-absolute path, make it absolute.
65 65 self.notebook_dir = os.path.abspath(new)
66 66 return
67 67 if os.path.exists(new) and not os.path.isdir(new):
68 68 raise TraitError("notebook dir %r is not a directory" % new)
69 69 if not os.path.exists(new):
70 70 self.log.info("Creating notebook dir %s", new)
71 71 try:
72 72 os.mkdir(new)
73 73 except:
74 74 raise TraitError("Couldn't create notebook dir %r" % new)
75 75
76 76 # Main notebook API
77 77
78 78 def increment_filename(self, basename, path=''):
79 79 """Increment a notebook filename without the .ipynb to make it unique.
80 80
81 81 Parameters
82 82 ----------
83 83 basename : unicode
84 84 The name of a notebook without the ``.ipynb`` file extension.
85 85 path : unicode
86 86 The URL path of the notebooks directory
87 87 """
88 88 return basename
89 89
90 90 def list_notebooks(self, path=''):
91 91 """Return a list of notebook dicts without content.
92 92
93 93 This returns a list of dicts, each of the form::
94 94
95 95 dict(notebook_id=notebook,name=name)
96 96
97 97 This list of dicts should be sorted by name::
98 98
99 99 data = sorted(data, key=lambda item: item['name'])
100 100 """
101 101 raise NotImplementedError('must be implemented in a subclass')
102 102
103 103 def get_notebook_model(self, name, path='', content=True):
104 104 """Get the notebook model with or without content."""
105 105 raise NotImplementedError('must be implemented in a subclass')
106 106
107 107 def save_notebook_model(self, model, name, path=''):
108 108 """Save the notebook model and return the model with no content."""
109 109 raise NotImplementedError('must be implemented in a subclass')
110 110
111 111 def update_notebook_model(self, model, name, path=''):
112 112 """Update the notebook model and return the model with no content."""
113 113 raise NotImplementedError('must be implemented in a subclass')
114 114
115 115 def delete_notebook_model(self, name, path=''):
116 116 """Delete notebook by name and path."""
117 117 raise NotImplementedError('must be implemented in a subclass')
118 118
119 119 def create_notebook_model(self, model=None, path=''):
120 120 """Create a new untitled notebook and return its model with no content."""
121 path = path.strip('/')
121 122 if model is None:
122 123 model = {}
123 124 if 'content' not in model:
124 125 metadata = current.new_metadata(name=u'')
125 126 model['content'] = current.new_notebook(metadata=metadata)
126 127 if 'name' not in model:
127 128 model['name'] = self.increment_filename('Untitled', path)
128 129
129 130 model['path'] = path
130 131 model = self.save_notebook_model(model, model['name'], model['path'])
131 132 return model
132 133
133 134 def copy_notebook(self, name, path=''):
134 135 """Copy an existing notebook and return its new model."""
136 path = path.strip('/')
135 137 model = self.get_notebook_model(name, path)
136 138 name = os.path.splitext(name)[0] + '-Copy'
137 139 name = self.increment_filename(name, path)
138 140 model['name'] = name
139 141 model = self.save_notebook_model(model, name, path)
140 142 return model
141 143
142 144 # Checkpoint-related
143 145
144 146 def create_checkpoint(self, name, path=''):
145 147 """Create a checkpoint of the current state of a notebook
146 148
147 149 Returns a checkpoint_id for the new checkpoint.
148 150 """
149 151 raise NotImplementedError("must be implemented in a subclass")
150 152
151 153 def list_checkpoints(self, name, path=''):
152 154 """Return a list of checkpoints for a given notebook"""
153 155 return []
154 156
155 157 def restore_checkpoint(self, checkpoint_id, name, path=''):
156 158 """Restore a notebook from one of its checkpoints"""
157 159 raise NotImplementedError("must be implemented in a subclass")
158 160
159 161 def delete_checkpoint(self, checkpoint_id, name, path=''):
160 162 """delete a checkpoint for a notebook"""
161 163 raise NotImplementedError("must be implemented in a subclass")
162 164
163 165 def log_info(self):
164 166 self.log.info(self.info_string())
165 167
166 168 def info_string(self):
167 169 return "Serving notebooks"
General Comments 0
You need to be logged in to leave comments. Login now