##// END OF EJS Templates
normalize unicode notebook filenames...
MinRK -
Show More
@@ -1,346 +1,347 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 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import datetime
20 20 import io
21 21 import os
22 22 import glob
23 23 import shutil
24 from unicodedata import normalize
24 25
25 26 from tornado import web
26 27
27 28 from .nbmanager import NotebookManager
28 29 from IPython.nbformat import current
29 30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 31
31 32 #-----------------------------------------------------------------------------
32 33 # Classes
33 34 #-----------------------------------------------------------------------------
34 35
35 36 class FileNotebookManager(NotebookManager):
36 37
37 38 save_script = Bool(False, config=True,
38 39 help="""Automatically create a Python script when saving the notebook.
39 40
40 41 For easier use of import, %run and %load across notebooks, a
41 42 <notebook-name>.py script will be created next to any
42 43 <notebook-name>.ipynb on each save. This can also be set with the
43 44 short `--script` flag.
44 45 """
45 46 )
46 47
47 48 checkpoint_dir = Unicode(config=True,
48 49 help="""The location in which to keep notebook checkpoints
49 50
50 51 By default, it is notebook-dir/.ipynb_checkpoints
51 52 """
52 53 )
53 54 def _checkpoint_dir_default(self):
54 55 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
55 56
56 57 def _checkpoint_dir_changed(self, name, old, new):
57 58 """do a bit of validation of the checkpoint dir"""
58 59 if not os.path.isabs(new):
59 60 # If we receive a non-absolute path, make it absolute.
60 61 abs_new = os.path.abspath(new)
61 62 self.checkpoint_dir = abs_new
62 63 return
63 64 if os.path.exists(new) and not os.path.isdir(new):
64 65 raise TraitError("checkpoint dir %r is not a directory" % new)
65 66 if not os.path.exists(new):
66 67 self.log.info("Creating checkpoint dir %s", new)
67 68 try:
68 69 os.mkdir(new)
69 70 except:
70 71 raise TraitError("Couldn't create checkpoint dir %r" % new)
71 72
72 73 filename_ext = Unicode(u'.ipynb')
73 74
74 75 # Map notebook names to notebook_ids
75 76 rev_mapping = Dict()
76 77
77 78 def get_notebook_names(self):
78 79 """List all notebook names in the notebook dir."""
79 80 names = glob.glob(os.path.join(self.notebook_dir,
80 81 '*' + self.filename_ext))
81 names = [os.path.splitext(os.path.basename(name))[0]
82 names = [normalize('NFC', os.path.splitext(os.path.basename(name))[0])
82 83 for name in names]
83 84 return names
84 85
85 86 def list_notebooks(self):
86 87 """List all notebooks in the notebook dir."""
87 88 names = self.get_notebook_names()
88 89
89 90 data = []
90 91 for name in names:
91 92 if name not in self.rev_mapping:
92 93 notebook_id = self.new_notebook_id(name)
93 94 else:
94 95 notebook_id = self.rev_mapping[name]
95 96 data.append(dict(notebook_id=notebook_id,name=name))
96 97 data = sorted(data, key=lambda item: item['name'])
97 98 return data
98 99
99 100 def new_notebook_id(self, name):
100 101 """Generate a new notebook_id for a name and store its mappings."""
101 102 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
102 103 self.rev_mapping[name] = notebook_id
103 104 return notebook_id
104 105
105 106 def delete_notebook_id(self, notebook_id):
106 107 """Delete a notebook's id in the mapping."""
107 108 name = self.mapping[notebook_id]
108 109 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
109 110 del self.rev_mapping[name]
110 111
111 112 def notebook_exists(self, notebook_id):
112 113 """Does a notebook exist?"""
113 114 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
114 115 if not exists:
115 116 return False
116 117 path = self.get_path_by_name(self.mapping[notebook_id])
117 118 return os.path.isfile(path)
118 119
119 120 def get_name(self, notebook_id):
120 121 """get a notebook name, raising 404 if not found"""
121 122 try:
122 123 name = self.mapping[notebook_id]
123 124 except KeyError:
124 125 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
125 126 return name
126 127
127 128 def get_path(self, notebook_id):
128 129 """Return a full path to a notebook given its notebook_id."""
129 130 name = self.get_name(notebook_id)
130 131 return self.get_path_by_name(name)
131 132
132 133 def get_path_by_name(self, name):
133 134 """Return a full path to a notebook given its name."""
134 135 filename = name + self.filename_ext
135 136 path = os.path.join(self.notebook_dir, filename)
136 137 return path
137 138
138 139 def read_notebook_object_from_path(self, path):
139 140 """read a notebook object from a path"""
140 141 info = os.stat(path)
141 142 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
142 143 with open(path,'r') as f:
143 144 s = f.read()
144 145 try:
145 146 # v1 and v2 and json in the .ipynb files.
146 147 nb = current.reads(s, u'json')
147 148 except Exception as e:
148 149 raise web.HTTPError(500, u'Unreadable JSON notebook: %s' % e)
149 150 return last_modified, nb
150 151
151 152 def read_notebook_object(self, notebook_id):
152 153 """Get the Notebook representation of a notebook by notebook_id."""
153 154 path = self.get_path(notebook_id)
154 155 if not os.path.isfile(path):
155 156 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
156 157 last_modified, nb = self.read_notebook_object_from_path(path)
157 158 # Always use the filename as the notebook name.
158 159 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
159 160 return last_modified, nb
160 161
161 162 def write_notebook_object(self, nb, notebook_id=None):
162 163 """Save an existing notebook object by notebook_id."""
163 164 try:
164 new_name = nb.metadata.name
165 new_name = normalize('NFC', nb.metadata.name)
165 166 except AttributeError:
166 167 raise web.HTTPError(400, u'Missing notebook name')
167 168
168 169 if notebook_id is None:
169 170 notebook_id = self.new_notebook_id(new_name)
170 171
171 172 if notebook_id not in self.mapping:
172 173 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
173 174
174 175 old_name = self.mapping[notebook_id]
175 176 old_checkpoints = self.list_checkpoints(notebook_id)
176 177
177 178 path = self.get_path_by_name(new_name)
178 179 try:
179 180 self.log.debug("Autosaving notebook %s", path)
180 181 with open(path,'w') as f:
181 182 current.write(nb, f, u'json')
182 183 except Exception as e:
183 184 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s' % e)
184 185
185 186 # save .py script as well
186 187 if self.save_script:
187 188 pypath = os.path.splitext(path)[0] + '.py'
188 189 self.log.debug("Writing script %s", pypath)
189 190 try:
190 191 with io.open(pypath,'w', encoding='utf-8') as f:
191 192 current.write(nb, f, u'py')
192 193 except Exception as e:
193 194 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
194 195
195 196 # remove old files if the name changed
196 197 if old_name != new_name:
197 198 # update mapping
198 199 self.mapping[notebook_id] = new_name
199 200 self.rev_mapping[new_name] = notebook_id
200 201 del self.rev_mapping[old_name]
201 202
202 203 # remove renamed original, if it exists
203 204 old_path = self.get_path_by_name(old_name)
204 205 if os.path.isfile(old_path):
205 206 self.log.debug("unlinking notebook %s", old_path)
206 207 os.unlink(old_path)
207 208
208 209 # cleanup old script, if it exists
209 210 if self.save_script:
210 211 old_pypath = os.path.splitext(old_path)[0] + '.py'
211 212 if os.path.isfile(old_pypath):
212 213 self.log.debug("unlinking script %s", old_pypath)
213 214 os.unlink(old_pypath)
214 215
215 216 # rename checkpoints to follow file
216 217 for cp in old_checkpoints:
217 218 checkpoint_id = cp['checkpoint_id']
218 219 old_cp_path = self.get_checkpoint_path_by_name(old_name, checkpoint_id)
219 220 new_cp_path = self.get_checkpoint_path_by_name(new_name, checkpoint_id)
220 221 if os.path.isfile(old_cp_path):
221 222 self.log.debug("renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
222 223 os.rename(old_cp_path, new_cp_path)
223 224
224 225 return notebook_id
225 226
226 227 def delete_notebook(self, notebook_id):
227 228 """Delete notebook by notebook_id."""
228 229 nb_path = self.get_path(notebook_id)
229 230 if not os.path.isfile(nb_path):
230 231 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
231 232
232 233 # clear checkpoints
233 234 for checkpoint in self.list_checkpoints(notebook_id):
234 235 checkpoint_id = checkpoint['checkpoint_id']
235 236 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
236 237 self.log.debug(path)
237 238 if os.path.isfile(path):
238 239 self.log.debug("unlinking checkpoint %s", path)
239 240 os.unlink(path)
240 241
241 242 self.log.debug("unlinking notebook %s", nb_path)
242 243 os.unlink(nb_path)
243 244 self.delete_notebook_id(notebook_id)
244 245
245 246 def increment_filename(self, basename):
246 247 """Return a non-used filename of the form basename<int>.
247 248
248 249 This searches through the filenames (basename0, basename1, ...)
249 250 until is find one that is not already being used. It is used to
250 251 create Untitled and Copy names that are unique.
251 252 """
252 253 i = 0
253 254 while True:
254 255 name = u'%s%i' % (basename,i)
255 256 path = self.get_path_by_name(name)
256 257 if not os.path.isfile(path):
257 258 break
258 259 else:
259 260 i = i+1
260 261 return name
261 262
262 263 # Checkpoint-related utilities
263 264
264 265 def get_checkpoint_path_by_name(self, name, checkpoint_id):
265 266 """Return a full path to a notebook checkpoint, given its name and checkpoint id."""
266 filename = "{name}-{checkpoint_id}{ext}".format(
267 filename = u"{name}-{checkpoint_id}{ext}".format(
267 268 name=name,
268 269 checkpoint_id=checkpoint_id,
269 270 ext=self.filename_ext,
270 271 )
271 272 path = os.path.join(self.checkpoint_dir, filename)
272 273 return path
273 274
274 275 def get_checkpoint_path(self, notebook_id, checkpoint_id):
275 276 """find the path to a checkpoint"""
276 277 name = self.get_name(notebook_id)
277 278 return self.get_checkpoint_path_by_name(name, checkpoint_id)
278 279
279 280 def get_checkpoint_info(self, notebook_id, checkpoint_id):
280 281 """construct the info dict for a given checkpoint"""
281 282 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
282 283 stats = os.stat(path)
283 284 last_modified = datetime.datetime.utcfromtimestamp(stats.st_mtime)
284 285 info = dict(
285 286 checkpoint_id = checkpoint_id,
286 287 last_modified = last_modified,
287 288 )
288 289
289 290 return info
290 291
291 292 # public checkpoint API
292 293
293 294 def create_checkpoint(self, notebook_id):
294 295 """Create a checkpoint from the current state of a notebook"""
295 296 nb_path = self.get_path(notebook_id)
296 297 # only the one checkpoint ID:
297 checkpoint_id = "checkpoint"
298 checkpoint_id = u"checkpoint"
298 299 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
299 300 self.log.debug("creating checkpoint for notebook %s", notebook_id)
300 301 if not os.path.exists(self.checkpoint_dir):
301 302 os.mkdir(self.checkpoint_dir)
302 303 shutil.copy2(nb_path, cp_path)
303 304
304 305 # return the checkpoint info
305 306 return self.get_checkpoint_info(notebook_id, checkpoint_id)
306 307
307 308 def list_checkpoints(self, notebook_id):
308 309 """list the checkpoints for a given notebook
309 310
310 311 This notebook manager currently only supports one checkpoint per notebook.
311 312 """
312 checkpoint_id = "checkpoint"
313 checkpoint_id = u"checkpoint"
313 314 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
314 315 if not os.path.exists(path):
315 316 return []
316 317 else:
317 318 return [self.get_checkpoint_info(notebook_id, checkpoint_id)]
318 319
319 320
320 321 def restore_checkpoint(self, notebook_id, checkpoint_id):
321 322 """restore a notebook to a checkpointed state"""
322 323 self.log.info("restoring Notebook %s from checkpoint %s", notebook_id, checkpoint_id)
323 324 nb_path = self.get_path(notebook_id)
324 325 cp_path = self.get_checkpoint_path(notebook_id, checkpoint_id)
325 326 if not os.path.isfile(cp_path):
326 327 self.log.debug("checkpoint file does not exist: %s", cp_path)
327 328 raise web.HTTPError(404,
328 329 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
329 330 )
330 331 # ensure notebook is readable (never restore from an unreadable notebook)
331 332 last_modified, nb = self.read_notebook_object_from_path(cp_path)
332 333 shutil.copy2(cp_path, nb_path)
333 334 self.log.debug("copying %s -> %s", cp_path, nb_path)
334 335
335 336 def delete_checkpoint(self, notebook_id, checkpoint_id):
336 337 """delete a notebook's checkpoint"""
337 338 path = self.get_checkpoint_path(notebook_id, checkpoint_id)
338 339 if not os.path.isfile(path):
339 340 raise web.HTTPError(404,
340 341 u'Notebook checkpoint does not exist: %s-%s' % (notebook_id, checkpoint_id)
341 342 )
342 343 self.log.debug("unlinking %s", path)
343 344 os.unlink(path)
344 345
345 346 def info_string(self):
346 347 return "Serving notebooks from local directory: %s" % self.notebook_dir
General Comments 0
You need to be logged in to leave comments. Login now