##// END OF EJS Templates
404 on attempt to list notebooks in nonexistant directory
Thomas Kluyver -
Show More
@@ -1,412 +1,414 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 78 path = path.strip('/')
79 if not os.path.isdir(self.get_os_path(path=path)):
80 raise web.HTTPError(404, 'Directory not found: ' + path)
79 81 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 82 names = [os.path.basename(name)
81 83 for name in names]
82 84 return names
83 85
84 86 def increment_filename(self, basename, path=''):
85 87 """Return a non-used filename of the form basename<int>."""
86 88 path = path.strip('/')
87 89 i = 0
88 90 while True:
89 91 name = u'%s%i.ipynb' % (basename,i)
90 92 os_path = self.get_os_path(name, path)
91 93 if not os.path.isfile(os_path):
92 94 break
93 95 else:
94 96 i = i+1
95 97 return name
96 98
97 99 def path_exists(self, path):
98 100 """Does the API-style path (directory) actually exist?
99 101
100 102 Parameters
101 103 ----------
102 104 path : string
103 105 The path to check. This is an API path (`/` separated,
104 106 relative to base notebook-dir).
105 107
106 108 Returns
107 109 -------
108 110 exists : bool
109 111 Whether the path is indeed a directory.
110 112 """
111 113 path = path.strip('/')
112 114 os_path = self.get_os_path(path=path)
113 115 return os.path.isdir(os_path)
114 116
115 117 def get_os_path(self, name=None, path=''):
116 118 """Given a notebook name and a URL path, return its file system
117 119 path.
118 120
119 121 Parameters
120 122 ----------
121 123 name : string
122 124 The name of a notebook file with the .ipynb extension
123 125 path : string
124 126 The relative URL path (with '/' as separator) to the named
125 127 notebook.
126 128
127 129 Returns
128 130 -------
129 131 path : string
130 132 A file system path that combines notebook_dir (location where
131 133 server started), the relative path, and the filename with the
132 134 current operating system's url.
133 135 """
134 136 parts = path.strip('/').split('/')
135 137 parts = [p for p in parts if p != ''] # remove duplicate splits
136 138 if name is not None:
137 139 parts.append(name)
138 140 path = os.path.join(self.notebook_dir, *parts)
139 141 return path
140 142
141 143 def notebook_exists(self, name, path=''):
142 144 """Returns a True if the notebook exists. Else, returns False.
143 145
144 146 Parameters
145 147 ----------
146 148 name : string
147 149 The name of the notebook you are checking.
148 150 path : string
149 151 The relative path to the notebook (with '/' as separator)
150 152
151 153 Returns
152 154 -------
153 155 bool
154 156 """
155 157 path = path.strip('/')
156 158 nbpath = self.get_os_path(name, path=path)
157 159 return os.path.isfile(nbpath)
158 160
159 161 def list_notebooks(self, path):
160 162 """Returns a list of dictionaries that are the standard model
161 163 for all notebooks in the relative 'path'.
162 164
163 165 Parameters
164 166 ----------
165 167 path : str
166 168 the URL path that describes the relative path for the
167 169 listed notebooks
168 170
169 171 Returns
170 172 -------
171 173 notebooks : list of dicts
172 174 a list of the notebook models without 'content'
173 175 """
174 176 path = path.strip('/')
175 177 notebook_names = self.get_notebook_names(path)
176 178 notebooks = []
177 179 for name in notebook_names:
178 180 model = self.get_notebook_model(name, path, content=False)
179 181 notebooks.append(model)
180 182 notebooks = sorted(notebooks, key=lambda item: item['name'])
181 183 return notebooks
182 184
183 185 def get_notebook_model(self, name, path='', content=True):
184 186 """ Takes a path and name for a notebook and returns it's model
185 187
186 188 Parameters
187 189 ----------
188 190 name : str
189 191 the name of the notebook
190 192 path : str
191 193 the URL path that describes the relative path for
192 194 the notebook
193 195
194 196 Returns
195 197 -------
196 198 model : dict
197 199 the notebook model. If contents=True, returns the 'contents'
198 200 dict in the model as well.
199 201 """
200 202 path = path.strip('/')
201 203 if not self.notebook_exists(name=name, path=path):
202 204 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
203 205 os_path = self.get_os_path(name, path)
204 206 info = os.stat(os_path)
205 207 last_modified = tz.utcfromtimestamp(info.st_mtime)
206 208 created = tz.utcfromtimestamp(info.st_ctime)
207 209 # Create the notebook model.
208 210 model ={}
209 211 model['name'] = name
210 212 model['path'] = path
211 213 model['last_modified'] = last_modified
212 214 model['created'] = last_modified
213 215 if content is True:
214 216 with open(os_path, 'r') as f:
215 217 try:
216 218 nb = current.read(f, u'json')
217 219 except Exception as e:
218 220 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
219 221 model['content'] = nb
220 222 return model
221 223
222 224 def save_notebook_model(self, model, name='', path=''):
223 225 """Save the notebook model and return the model with no content."""
224 226 path = path.strip('/')
225 227
226 228 if 'content' not in model:
227 229 raise web.HTTPError(400, u'No notebook JSON data provided')
228 230
229 231 new_path = model.get('path', path).strip('/')
230 232 new_name = model.get('name', name)
231 233
232 234 if path != new_path or name != new_name:
233 235 self.rename_notebook(name, path, new_name, new_path)
234 236
235 237 # Save the notebook file
236 238 os_path = self.get_os_path(new_name, new_path)
237 239 nb = current.to_notebook_json(model['content'])
238 240 if 'name' in nb['metadata']:
239 241 nb['metadata']['name'] = u''
240 242 try:
241 243 self.log.debug("Autosaving notebook %s", os_path)
242 244 with open(os_path, 'w') as f:
243 245 current.write(nb, f, u'json')
244 246 except Exception as e:
245 247 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
246 248
247 249 # Save .py script as well
248 250 if self.save_script:
249 251 py_path = os.path.splitext(os_path)[0] + '.py'
250 252 self.log.debug("Writing script %s", py_path)
251 253 try:
252 254 with io.open(py_path, 'w', encoding='utf-8') as f:
253 255 current.write(model, f, u'py')
254 256 except Exception as e:
255 257 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
256 258
257 259 model = self.get_notebook_model(new_name, new_path, content=False)
258 260 return model
259 261
260 262 def update_notebook_model(self, model, name, path=''):
261 263 """Update the notebook's path and/or name"""
262 264 path = path.strip('/')
263 265 new_name = model.get('name', name)
264 266 new_path = model.get('path', path).strip('/')
265 267 if path != new_path or name != new_name:
266 268 self.rename_notebook(name, path, new_name, new_path)
267 269 model = self.get_notebook_model(new_name, new_path, content=False)
268 270 return model
269 271
270 272 def delete_notebook_model(self, name, path=''):
271 273 """Delete notebook by name and path."""
272 274 path = path.strip('/')
273 275 os_path = self.get_os_path(name, path)
274 276 if not os.path.isfile(os_path):
275 277 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
276 278
277 279 # clear checkpoints
278 280 for checkpoint in self.list_checkpoints(name, path):
279 281 checkpoint_id = checkpoint['checkpoint_id']
280 282 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
281 283 if os.path.isfile(cp_path):
282 284 self.log.debug("Unlinking checkpoint %s", cp_path)
283 285 os.unlink(cp_path)
284 286
285 287 self.log.debug("Unlinking notebook %s", os_path)
286 288 os.unlink(os_path)
287 289
288 290 def rename_notebook(self, old_name, old_path, new_name, new_path):
289 291 """Rename a notebook."""
290 292 old_path = old_path.strip('/')
291 293 new_path = new_path.strip('/')
292 294 if new_name == old_name and new_path == old_path:
293 295 return
294 296
295 297 new_os_path = self.get_os_path(new_name, new_path)
296 298 old_os_path = self.get_os_path(old_name, old_path)
297 299
298 300 # Should we proceed with the move?
299 301 if os.path.isfile(new_os_path):
300 302 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
301 303 if self.save_script:
302 304 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
303 305 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
304 306 if os.path.isfile(new_py_path):
305 307 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
306 308
307 309 # Move the notebook file
308 310 try:
309 311 os.rename(old_os_path, new_os_path)
310 312 except Exception as e:
311 313 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
312 314
313 315 # Move the checkpoints
314 316 old_checkpoints = self.list_checkpoints(old_name, old_path)
315 317 for cp in old_checkpoints:
316 318 checkpoint_id = cp['checkpoint_id']
317 319 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
318 320 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
319 321 if os.path.isfile(old_cp_path):
320 322 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
321 323 os.rename(old_cp_path, new_cp_path)
322 324
323 325 # Move the .py script
324 326 if self.save_script:
325 327 os.rename(old_py_path, new_py_path)
326 328
327 329 # Checkpoint-related utilities
328 330
329 331 def get_checkpoint_path(self, checkpoint_id, name, path=''):
330 332 """find the path to a checkpoint"""
331 333 path = path.strip('/')
332 334 filename = u"{name}-{checkpoint_id}{ext}".format(
333 335 name=name,
334 336 checkpoint_id=checkpoint_id,
335 337 ext=self.filename_ext,
336 338 )
337 339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
338 340 return cp_path
339 341
340 342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
341 343 """construct the info dict for a given checkpoint"""
342 344 path = path.strip('/')
343 345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
344 346 stats = os.stat(cp_path)
345 347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
346 348 info = dict(
347 349 checkpoint_id = checkpoint_id,
348 350 last_modified = last_modified,
349 351 )
350 352 return info
351 353
352 354 # public checkpoint API
353 355
354 356 def create_checkpoint(self, name, path=''):
355 357 """Create a checkpoint from the current state of a notebook"""
356 358 path = path.strip('/')
357 359 nb_path = self.get_os_path(name, path)
358 360 # only the one checkpoint ID:
359 361 checkpoint_id = u"checkpoint"
360 362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
361 363 self.log.debug("creating checkpoint for notebook %s", name)
362 364 if not os.path.exists(self.checkpoint_dir):
363 365 os.mkdir(self.checkpoint_dir)
364 366 shutil.copy2(nb_path, cp_path)
365 367
366 368 # return the checkpoint info
367 369 return self.get_checkpoint_model(checkpoint_id, name, path)
368 370
369 371 def list_checkpoints(self, name, path=''):
370 372 """list the checkpoints for a given notebook
371 373
372 374 This notebook manager currently only supports one checkpoint per notebook.
373 375 """
374 376 path = path.strip('/')
375 377 checkpoint_id = "checkpoint"
376 378 path = self.get_checkpoint_path(checkpoint_id, name, path)
377 379 if not os.path.exists(path):
378 380 return []
379 381 else:
380 382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
381 383
382 384
383 385 def restore_checkpoint(self, checkpoint_id, name, path=''):
384 386 """restore a notebook to a checkpointed state"""
385 387 path = path.strip('/')
386 388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
387 389 nb_path = self.get_os_path(name, path)
388 390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
389 391 if not os.path.isfile(cp_path):
390 392 self.log.debug("checkpoint file does not exist: %s", cp_path)
391 393 raise web.HTTPError(404,
392 394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
393 395 )
394 396 # ensure notebook is readable (never restore from an unreadable notebook)
395 397 with file(cp_path, 'r') as f:
396 398 nb = current.read(f, u'json')
397 399 shutil.copy2(cp_path, nb_path)
398 400 self.log.debug("copying %s -> %s", cp_path, nb_path)
399 401
400 402 def delete_checkpoint(self, checkpoint_id, name, path=''):
401 403 """delete a notebook's checkpoint"""
402 404 path = path.strip('/')
403 405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
404 406 if not os.path.isfile(cp_path):
405 407 raise web.HTTPError(404,
406 408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
407 409 )
408 410 self.log.debug("unlinking %s", cp_path)
409 411 os.unlink(cp_path)
410 412
411 413 def info_string(self):
412 414 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now