##// END OF EJS Templates
update `--script` behavior to use `nbconvert --to script`
Min RK -
Show More
@@ -1,690 +1,693 b''
1 """A contents manager that uses the local file system for storage."""
1 """A contents manager that uses the local file system for storage."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import base64
6 import base64
7 import errno
7 import errno
8 import io
8 import io
9 import os
9 import os
10 import shutil
10 import shutil
11 from contextlib import contextmanager
11 from contextlib import contextmanager
12 import mimetypes
12 import mimetypes
13
13
14 from tornado import web
14 from tornado import web
15
15
16 from .manager import ContentsManager
16 from .manager import ContentsManager
17 from IPython import nbformat
17 from IPython import nbformat
18 from IPython.utils.io import atomic_writing
18 from IPython.utils.io import atomic_writing
19 from IPython.utils.importstring import import_item
19 from IPython.utils.importstring import import_item
20 from IPython.utils.path import ensure_dir_exists
20 from IPython.utils.path import ensure_dir_exists
21 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
21 from IPython.utils.traitlets import Any, Unicode, Bool, TraitError
22 from IPython.utils.py3compat import getcwd, str_to_unicode
22 from IPython.utils.py3compat import getcwd, str_to_unicode, string_types
23 from IPython.utils import tz
23 from IPython.utils import tz
24 from IPython.html.utils import is_hidden, to_os_path, to_api_path
24 from IPython.html.utils import is_hidden, to_os_path, to_api_path
25
25
26 _script_exporter = None
26 _script_exporter = None
27
27 def _post_save_script(model, os_path, contents_manager, **kwargs):
28 def _post_save_script(model, os_path, contents_manager, **kwargs):
28 """convert notebooks to Python script after save with nbconvert
29 """convert notebooks to Python script after save with nbconvert
29
30
30 replaces `ipython notebook --script`
31 replaces `ipython notebook --script`
31 """
32 """
32 from IPython.nbconvert.exporters.python import PythonExporter
33 from IPython.nbconvert.exporters.script import ScriptExporter
33
34
34 if model['type'] != 'notebook':
35 if model['type'] != 'notebook':
35 return
36 return
37
36 global _script_exporter
38 global _script_exporter
37 if _script_exporter is None:
39 if _script_exporter is None:
38 _script_exporter = PythonExporter(parent=contents_manager)
40 _script_exporter = ScriptExporter(parent=contents_manager)
39 log = contents_manager.log
41 log = contents_manager.log
40
42
41 base, ext = os.path.splitext(os_path)
43 base, ext = os.path.splitext(os_path)
42 py_fname = base + '.py'
44 py_fname = base + '.py'
43 log.info("Writing %s", py_fname)
45 script, resources = _script_exporter.from_filename(os_path)
44 py, resources = _script_exporter.from_filename(os_path)
46 script_fname = base + resources.get('output_extension', '.txt')
45 with io.open(py_fname, 'w', encoding='utf-8') as f:
47 log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir))
46 f.write(py)
48 with io.open(script_fname, 'w', encoding='utf-8') as f:
49 f.write(script)
47
50
48 class FileContentsManager(ContentsManager):
51 class FileContentsManager(ContentsManager):
49
52
50 root_dir = Unicode(config=True)
53 root_dir = Unicode(config=True)
51
54
52 def _root_dir_default(self):
55 def _root_dir_default(self):
53 try:
56 try:
54 return self.parent.notebook_dir
57 return self.parent.notebook_dir
55 except AttributeError:
58 except AttributeError:
56 return getcwd()
59 return getcwd()
57
60
58 @contextmanager
61 @contextmanager
59 def perm_to_403(self, os_path=''):
62 def perm_to_403(self, os_path=''):
60 """context manager for turning permission errors into 403"""
63 """context manager for turning permission errors into 403"""
61 try:
64 try:
62 yield
65 yield
63 except OSError as e:
66 except OSError as e:
64 if e.errno in {errno.EPERM, errno.EACCES}:
67 if e.errno in {errno.EPERM, errno.EACCES}:
65 # make 403 error message without root prefix
68 # make 403 error message without root prefix
66 # this may not work perfectly on unicode paths on Python 2,
69 # this may not work perfectly on unicode paths on Python 2,
67 # but nobody should be doing that anyway.
70 # but nobody should be doing that anyway.
68 if not os_path:
71 if not os_path:
69 os_path = str_to_unicode(e.filename or 'unknown file')
72 os_path = str_to_unicode(e.filename or 'unknown file')
70 path = to_api_path(os_path, self.root_dir)
73 path = to_api_path(os_path, self.root_dir)
71 raise web.HTTPError(403, u'Permission denied: %s' % path)
74 raise web.HTTPError(403, u'Permission denied: %s' % path)
72 else:
75 else:
73 raise
76 raise
74
77
75 @contextmanager
78 @contextmanager
76 def open(self, os_path, *args, **kwargs):
79 def open(self, os_path, *args, **kwargs):
77 """wrapper around io.open that turns permission errors into 403"""
80 """wrapper around io.open that turns permission errors into 403"""
78 with self.perm_to_403(os_path):
81 with self.perm_to_403(os_path):
79 with io.open(os_path, *args, **kwargs) as f:
82 with io.open(os_path, *args, **kwargs) as f:
80 yield f
83 yield f
81
84
82 @contextmanager
85 @contextmanager
83 def atomic_writing(self, os_path, *args, **kwargs):
86 def atomic_writing(self, os_path, *args, **kwargs):
84 """wrapper around atomic_writing that turns permission errors into 403"""
87 """wrapper around atomic_writing that turns permission errors into 403"""
85 with self.perm_to_403(os_path):
88 with self.perm_to_403(os_path):
86 with atomic_writing(os_path, *args, **kwargs) as f:
89 with atomic_writing(os_path, *args, **kwargs) as f:
87 yield f
90 yield f
88
91
89 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
92 save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook')
90 def _save_script_changed(self):
93 def _save_script_changed(self):
91 self.log.warn("""
94 self.log.warn("""
92 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
95 `--script` is deprecated. You can trigger nbconvert via pre- or post-save hooks:
93
96
94 ContentsManager.pre_save_hook
97 ContentsManager.pre_save_hook
95 FileContentsManager.post_save_hook
98 FileContentsManager.post_save_hook
96
99
97 A post-save hook has been registered that calls:
100 A post-save hook has been registered that calls:
98
101
99 ipython nbconvert --to python [notebook]
102 ipython nbconvert --to script [notebook]
100
103
101 which behaves similar to `--script`.
104 which behaves similarly to `--script`.
102 """)
105 """)
103
106
104 self.post_save_hook = _post_save_script
107 self.post_save_hook = _post_save_script
105
108
106 post_save_hook = Any(None, config=True,
109 post_save_hook = Any(None, config=True,
107 help="""Python callable or importstring thereof
110 help="""Python callable or importstring thereof
108
111
109 to be called on the path of a file just saved.
112 to be called on the path of a file just saved.
110
113
111 This can be used to
114 This can be used to
112 This can be used to process the file on disk,
115 This can be used to process the file on disk,
113 such as converting the notebook to other formats, such as Python or HTML via nbconvert
116 such as converting the notebook to other formats, such as Python or HTML via nbconvert
114
117
115 It will be called as (all arguments passed by keyword):
118 It will be called as (all arguments passed by keyword):
116
119
117 hook(os_path=os_path, model=model, contents_manager=instance)
120 hook(os_path=os_path, model=model, contents_manager=instance)
118
121
119 path: the filesystem path to the file just written
122 path: the filesystem path to the file just written
120 model: the model representing the file
123 model: the model representing the file
121 contents_manager: this ContentsManager instance
124 contents_manager: this ContentsManager instance
122 """
125 """
123 )
126 )
124 def _post_save_hook_changed(self, name, old, new):
127 def _post_save_hook_changed(self, name, old, new):
125 if new and isinstance(new, string_types):
128 if new and isinstance(new, string_types):
126 self.post_save_hook = import_item(self.post_save_hook)
129 self.post_save_hook = import_item(self.post_save_hook)
127 elif new:
130 elif new:
128 if not callable(new):
131 if not callable(new):
129 raise TraitError("post_save_hook must be callable")
132 raise TraitError("post_save_hook must be callable")
130
133
131 def run_post_save_hook(self, model, os_path):
134 def run_post_save_hook(self, model, os_path):
132 """Run the post-save hook if defined, and log errors"""
135 """Run the post-save hook if defined, and log errors"""
133 if self.post_save_hook:
136 if self.post_save_hook:
134 try:
137 try:
135 self.log.debug("Running post-save hook on %s", os_path)
138 self.log.debug("Running post-save hook on %s", os_path)
136 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
139 self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
137 except Exception:
140 except Exception:
138 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
141 self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
139
142
140 def _root_dir_changed(self, name, old, new):
143 def _root_dir_changed(self, name, old, new):
141 """Do a bit of validation of the root_dir."""
144 """Do a bit of validation of the root_dir."""
142 if not os.path.isabs(new):
145 if not os.path.isabs(new):
143 # If we receive a non-absolute path, make it absolute.
146 # If we receive a non-absolute path, make it absolute.
144 self.root_dir = os.path.abspath(new)
147 self.root_dir = os.path.abspath(new)
145 return
148 return
146 if not os.path.isdir(new):
149 if not os.path.isdir(new):
147 raise TraitError("%r is not a directory" % new)
150 raise TraitError("%r is not a directory" % new)
148
151
149 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
152 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
150 help="""The directory name in which to keep file checkpoints
153 help="""The directory name in which to keep file checkpoints
151
154
152 This is a path relative to the file's own directory.
155 This is a path relative to the file's own directory.
153
156
154 By default, it is .ipynb_checkpoints
157 By default, it is .ipynb_checkpoints
155 """
158 """
156 )
159 )
157
160
158 def _copy(self, src, dest):
161 def _copy(self, src, dest):
159 """copy src to dest
162 """copy src to dest
160
163
161 like shutil.copy2, but log errors in copystat
164 like shutil.copy2, but log errors in copystat
162 """
165 """
163 shutil.copyfile(src, dest)
166 shutil.copyfile(src, dest)
164 try:
167 try:
165 shutil.copystat(src, dest)
168 shutil.copystat(src, dest)
166 except OSError as e:
169 except OSError as e:
167 self.log.debug("copystat on %s failed", dest, exc_info=True)
170 self.log.debug("copystat on %s failed", dest, exc_info=True)
168
171
169 def _get_os_path(self, path):
172 def _get_os_path(self, path):
170 """Given an API path, return its file system path.
173 """Given an API path, return its file system path.
171
174
172 Parameters
175 Parameters
173 ----------
176 ----------
174 path : string
177 path : string
175 The relative API path to the named file.
178 The relative API path to the named file.
176
179
177 Returns
180 Returns
178 -------
181 -------
179 path : string
182 path : string
180 Native, absolute OS path to for a file.
183 Native, absolute OS path to for a file.
181 """
184 """
182 return to_os_path(path, self.root_dir)
185 return to_os_path(path, self.root_dir)
183
186
184 def dir_exists(self, path):
187 def dir_exists(self, path):
185 """Does the API-style path refer to an extant directory?
188 """Does the API-style path refer to an extant directory?
186
189
187 API-style wrapper for os.path.isdir
190 API-style wrapper for os.path.isdir
188
191
189 Parameters
192 Parameters
190 ----------
193 ----------
191 path : string
194 path : string
192 The path to check. This is an API path (`/` separated,
195 The path to check. This is an API path (`/` separated,
193 relative to root_dir).
196 relative to root_dir).
194
197
195 Returns
198 Returns
196 -------
199 -------
197 exists : bool
200 exists : bool
198 Whether the path is indeed a directory.
201 Whether the path is indeed a directory.
199 """
202 """
200 path = path.strip('/')
203 path = path.strip('/')
201 os_path = self._get_os_path(path=path)
204 os_path = self._get_os_path(path=path)
202 return os.path.isdir(os_path)
205 return os.path.isdir(os_path)
203
206
204 def is_hidden(self, path):
207 def is_hidden(self, path):
205 """Does the API style path correspond to a hidden directory or file?
208 """Does the API style path correspond to a hidden directory or file?
206
209
207 Parameters
210 Parameters
208 ----------
211 ----------
209 path : string
212 path : string
210 The path to check. This is an API path (`/` separated,
213 The path to check. This is an API path (`/` separated,
211 relative to root_dir).
214 relative to root_dir).
212
215
213 Returns
216 Returns
214 -------
217 -------
215 hidden : bool
218 hidden : bool
216 Whether the path exists and is hidden.
219 Whether the path exists and is hidden.
217 """
220 """
218 path = path.strip('/')
221 path = path.strip('/')
219 os_path = self._get_os_path(path=path)
222 os_path = self._get_os_path(path=path)
220 return is_hidden(os_path, self.root_dir)
223 return is_hidden(os_path, self.root_dir)
221
224
222 def file_exists(self, path):
225 def file_exists(self, path):
223 """Returns True if the file exists, else returns False.
226 """Returns True if the file exists, else returns False.
224
227
225 API-style wrapper for os.path.isfile
228 API-style wrapper for os.path.isfile
226
229
227 Parameters
230 Parameters
228 ----------
231 ----------
229 path : string
232 path : string
230 The relative path to the file (with '/' as separator)
233 The relative path to the file (with '/' as separator)
231
234
232 Returns
235 Returns
233 -------
236 -------
234 exists : bool
237 exists : bool
235 Whether the file exists.
238 Whether the file exists.
236 """
239 """
237 path = path.strip('/')
240 path = path.strip('/')
238 os_path = self._get_os_path(path)
241 os_path = self._get_os_path(path)
239 return os.path.isfile(os_path)
242 return os.path.isfile(os_path)
240
243
241 def exists(self, path):
244 def exists(self, path):
242 """Returns True if the path exists, else returns False.
245 """Returns True if the path exists, else returns False.
243
246
244 API-style wrapper for os.path.exists
247 API-style wrapper for os.path.exists
245
248
246 Parameters
249 Parameters
247 ----------
250 ----------
248 path : string
251 path : string
249 The API path to the file (with '/' as separator)
252 The API path to the file (with '/' as separator)
250
253
251 Returns
254 Returns
252 -------
255 -------
253 exists : bool
256 exists : bool
254 Whether the target exists.
257 Whether the target exists.
255 """
258 """
256 path = path.strip('/')
259 path = path.strip('/')
257 os_path = self._get_os_path(path=path)
260 os_path = self._get_os_path(path=path)
258 return os.path.exists(os_path)
261 return os.path.exists(os_path)
259
262
260 def _base_model(self, path):
263 def _base_model(self, path):
261 """Build the common base of a contents model"""
264 """Build the common base of a contents model"""
262 os_path = self._get_os_path(path)
265 os_path = self._get_os_path(path)
263 info = os.stat(os_path)
266 info = os.stat(os_path)
264 last_modified = tz.utcfromtimestamp(info.st_mtime)
267 last_modified = tz.utcfromtimestamp(info.st_mtime)
265 created = tz.utcfromtimestamp(info.st_ctime)
268 created = tz.utcfromtimestamp(info.st_ctime)
266 # Create the base model.
269 # Create the base model.
267 model = {}
270 model = {}
268 model['name'] = path.rsplit('/', 1)[-1]
271 model['name'] = path.rsplit('/', 1)[-1]
269 model['path'] = path
272 model['path'] = path
270 model['last_modified'] = last_modified
273 model['last_modified'] = last_modified
271 model['created'] = created
274 model['created'] = created
272 model['content'] = None
275 model['content'] = None
273 model['format'] = None
276 model['format'] = None
274 model['mimetype'] = None
277 model['mimetype'] = None
275 try:
278 try:
276 model['writable'] = os.access(os_path, os.W_OK)
279 model['writable'] = os.access(os_path, os.W_OK)
277 except OSError:
280 except OSError:
278 self.log.error("Failed to check write permissions on %s", os_path)
281 self.log.error("Failed to check write permissions on %s", os_path)
279 model['writable'] = False
282 model['writable'] = False
280 return model
283 return model
281
284
282 def _dir_model(self, path, content=True):
285 def _dir_model(self, path, content=True):
283 """Build a model for a directory
286 """Build a model for a directory
284
287
285 if content is requested, will include a listing of the directory
288 if content is requested, will include a listing of the directory
286 """
289 """
287 os_path = self._get_os_path(path)
290 os_path = self._get_os_path(path)
288
291
289 four_o_four = u'directory does not exist: %r' % path
292 four_o_four = u'directory does not exist: %r' % path
290
293
291 if not os.path.isdir(os_path):
294 if not os.path.isdir(os_path):
292 raise web.HTTPError(404, four_o_four)
295 raise web.HTTPError(404, four_o_four)
293 elif is_hidden(os_path, self.root_dir):
296 elif is_hidden(os_path, self.root_dir):
294 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
297 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
295 os_path
298 os_path
296 )
299 )
297 raise web.HTTPError(404, four_o_four)
300 raise web.HTTPError(404, four_o_four)
298
301
299 model = self._base_model(path)
302 model = self._base_model(path)
300 model['type'] = 'directory'
303 model['type'] = 'directory'
301 if content:
304 if content:
302 model['content'] = contents = []
305 model['content'] = contents = []
303 os_dir = self._get_os_path(path)
306 os_dir = self._get_os_path(path)
304 for name in os.listdir(os_dir):
307 for name in os.listdir(os_dir):
305 os_path = os.path.join(os_dir, name)
308 os_path = os.path.join(os_dir, name)
306 # skip over broken symlinks in listing
309 # skip over broken symlinks in listing
307 if not os.path.exists(os_path):
310 if not os.path.exists(os_path):
308 self.log.warn("%s doesn't exist", os_path)
311 self.log.warn("%s doesn't exist", os_path)
309 continue
312 continue
310 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
313 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
311 self.log.debug("%s not a regular file", os_path)
314 self.log.debug("%s not a regular file", os_path)
312 continue
315 continue
313 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
316 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
314 contents.append(self.get(
317 contents.append(self.get(
315 path='%s/%s' % (path, name),
318 path='%s/%s' % (path, name),
316 content=False)
319 content=False)
317 )
320 )
318
321
319 model['format'] = 'json'
322 model['format'] = 'json'
320
323
321 return model
324 return model
322
325
323 def _file_model(self, path, content=True, format=None):
326 def _file_model(self, path, content=True, format=None):
324 """Build a model for a file
327 """Build a model for a file
325
328
326 if content is requested, include the file contents.
329 if content is requested, include the file contents.
327
330
328 format:
331 format:
329 If 'text', the contents will be decoded as UTF-8.
332 If 'text', the contents will be decoded as UTF-8.
330 If 'base64', the raw bytes contents will be encoded as base64.
333 If 'base64', the raw bytes contents will be encoded as base64.
331 If not specified, try to decode as UTF-8, and fall back to base64
334 If not specified, try to decode as UTF-8, and fall back to base64
332 """
335 """
333 model = self._base_model(path)
336 model = self._base_model(path)
334 model['type'] = 'file'
337 model['type'] = 'file'
335
338
336 os_path = self._get_os_path(path)
339 os_path = self._get_os_path(path)
337 model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
340 model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain'
338
341
339 if content:
342 if content:
340 if not os.path.isfile(os_path):
343 if not os.path.isfile(os_path):
341 # could be FIFO
344 # could be FIFO
342 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
345 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
343 with self.open(os_path, 'rb') as f:
346 with self.open(os_path, 'rb') as f:
344 bcontent = f.read()
347 bcontent = f.read()
345
348
346 if format != 'base64':
349 if format != 'base64':
347 try:
350 try:
348 model['content'] = bcontent.decode('utf8')
351 model['content'] = bcontent.decode('utf8')
349 except UnicodeError as e:
352 except UnicodeError as e:
350 if format == 'text':
353 if format == 'text':
351 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
354 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
352 else:
355 else:
353 model['format'] = 'text'
356 model['format'] = 'text'
354
357
355 if model['content'] is None:
358 if model['content'] is None:
356 model['content'] = base64.encodestring(bcontent).decode('ascii')
359 model['content'] = base64.encodestring(bcontent).decode('ascii')
357 model['format'] = 'base64'
360 model['format'] = 'base64'
358
361
359 return model
362 return model
360
363
361
364
362 def _notebook_model(self, path, content=True):
365 def _notebook_model(self, path, content=True):
363 """Build a notebook model
366 """Build a notebook model
364
367
365 if content is requested, the notebook content will be populated
368 if content is requested, the notebook content will be populated
366 as a JSON structure (not double-serialized)
369 as a JSON structure (not double-serialized)
367 """
370 """
368 model = self._base_model(path)
371 model = self._base_model(path)
369 model['type'] = 'notebook'
372 model['type'] = 'notebook'
370 if content:
373 if content:
371 os_path = self._get_os_path(path)
374 os_path = self._get_os_path(path)
372 with self.open(os_path, 'r', encoding='utf-8') as f:
375 with self.open(os_path, 'r', encoding='utf-8') as f:
373 try:
376 try:
374 nb = nbformat.read(f, as_version=4)
377 nb = nbformat.read(f, as_version=4)
375 except Exception as e:
378 except Exception as e:
376 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
379 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
377 self.mark_trusted_cells(nb, path)
380 self.mark_trusted_cells(nb, path)
378 model['content'] = nb
381 model['content'] = nb
379 model['format'] = 'json'
382 model['format'] = 'json'
380 self.validate_notebook_model(model)
383 self.validate_notebook_model(model)
381 return model
384 return model
382
385
383 def get(self, path, content=True, type_=None, format=None):
386 def get(self, path, content=True, type_=None, format=None):
384 """ Takes a path for an entity and returns its model
387 """ Takes a path for an entity and returns its model
385
388
386 Parameters
389 Parameters
387 ----------
390 ----------
388 path : str
391 path : str
389 the API path that describes the relative path for the target
392 the API path that describes the relative path for the target
390 content : bool
393 content : bool
391 Whether to include the contents in the reply
394 Whether to include the contents in the reply
392 type_ : str, optional
395 type_ : str, optional
393 The requested type - 'file', 'notebook', or 'directory'.
396 The requested type - 'file', 'notebook', or 'directory'.
394 Will raise HTTPError 400 if the content doesn't match.
397 Will raise HTTPError 400 if the content doesn't match.
395 format : str, optional
398 format : str, optional
396 The requested format for file contents. 'text' or 'base64'.
399 The requested format for file contents. 'text' or 'base64'.
397 Ignored if this returns a notebook or directory model.
400 Ignored if this returns a notebook or directory model.
398
401
399 Returns
402 Returns
400 -------
403 -------
401 model : dict
404 model : dict
402 the contents model. If content=True, returns the contents
405 the contents model. If content=True, returns the contents
403 of the file or directory as well.
406 of the file or directory as well.
404 """
407 """
405 path = path.strip('/')
408 path = path.strip('/')
406
409
407 if not self.exists(path):
410 if not self.exists(path):
408 raise web.HTTPError(404, u'No such file or directory: %s' % path)
411 raise web.HTTPError(404, u'No such file or directory: %s' % path)
409
412
410 os_path = self._get_os_path(path)
413 os_path = self._get_os_path(path)
411 if os.path.isdir(os_path):
414 if os.path.isdir(os_path):
412 if type_ not in (None, 'directory'):
415 if type_ not in (None, 'directory'):
413 raise web.HTTPError(400,
416 raise web.HTTPError(400,
414 u'%s is a directory, not a %s' % (path, type_))
417 u'%s is a directory, not a %s' % (path, type_))
415 model = self._dir_model(path, content=content)
418 model = self._dir_model(path, content=content)
416 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
419 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
417 model = self._notebook_model(path, content=content)
420 model = self._notebook_model(path, content=content)
418 else:
421 else:
419 if type_ == 'directory':
422 if type_ == 'directory':
420 raise web.HTTPError(400,
423 raise web.HTTPError(400,
421 u'%s is not a directory')
424 u'%s is not a directory')
422 model = self._file_model(path, content=content, format=format)
425 model = self._file_model(path, content=content, format=format)
423 return model
426 return model
424
427
425 def _save_notebook(self, os_path, model, path=''):
428 def _save_notebook(self, os_path, model, path=''):
426 """save a notebook file"""
429 """save a notebook file"""
427 # Save the notebook file
430 # Save the notebook file
428 nb = nbformat.from_dict(model['content'])
431 nb = nbformat.from_dict(model['content'])
429
432
430 self.check_and_sign(nb, path)
433 self.check_and_sign(nb, path)
431
434
432 with self.atomic_writing(os_path, encoding='utf-8') as f:
435 with self.atomic_writing(os_path, encoding='utf-8') as f:
433 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
436 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
434
437
435 def _save_file(self, os_path, model, path=''):
438 def _save_file(self, os_path, model, path=''):
436 """save a non-notebook file"""
439 """save a non-notebook file"""
437 fmt = model.get('format', None)
440 fmt = model.get('format', None)
438 if fmt not in {'text', 'base64'}:
441 if fmt not in {'text', 'base64'}:
439 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
442 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
440 try:
443 try:
441 content = model['content']
444 content = model['content']
442 if fmt == 'text':
445 if fmt == 'text':
443 bcontent = content.encode('utf8')
446 bcontent = content.encode('utf8')
444 else:
447 else:
445 b64_bytes = content.encode('ascii')
448 b64_bytes = content.encode('ascii')
446 bcontent = base64.decodestring(b64_bytes)
449 bcontent = base64.decodestring(b64_bytes)
447 except Exception as e:
450 except Exception as e:
448 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
451 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
449 with self.atomic_writing(os_path, text=False) as f:
452 with self.atomic_writing(os_path, text=False) as f:
450 f.write(bcontent)
453 f.write(bcontent)
451
454
452 def _save_directory(self, os_path, model, path=''):
455 def _save_directory(self, os_path, model, path=''):
453 """create a directory"""
456 """create a directory"""
454 if is_hidden(os_path, self.root_dir):
457 if is_hidden(os_path, self.root_dir):
455 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
458 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
456 if not os.path.exists(os_path):
459 if not os.path.exists(os_path):
457 with self.perm_to_403():
460 with self.perm_to_403():
458 os.mkdir(os_path)
461 os.mkdir(os_path)
459 elif not os.path.isdir(os_path):
462 elif not os.path.isdir(os_path):
460 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
463 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
461 else:
464 else:
462 self.log.debug("Directory %r already exists", os_path)
465 self.log.debug("Directory %r already exists", os_path)
463
466
464 def save(self, model, path=''):
467 def save(self, model, path=''):
465 """Save the file model and return the model with no content."""
468 """Save the file model and return the model with no content."""
466 path = path.strip('/')
469 path = path.strip('/')
467
470
468 if 'type' not in model:
471 if 'type' not in model:
469 raise web.HTTPError(400, u'No file type provided')
472 raise web.HTTPError(400, u'No file type provided')
470 if 'content' not in model and model['type'] != 'directory':
473 if 'content' not in model and model['type'] != 'directory':
471 raise web.HTTPError(400, u'No file content provided')
474 raise web.HTTPError(400, u'No file content provided')
472
475
473 self.run_pre_save_hook(model=model, path=path)
476 self.run_pre_save_hook(model=model, path=path)
474
477
475 # One checkpoint should always exist
478 # One checkpoint should always exist
476 if self.file_exists(path) and not self.list_checkpoints(path):
479 if self.file_exists(path) and not self.list_checkpoints(path):
477 self.create_checkpoint(path)
480 self.create_checkpoint(path)
478
481
479 os_path = self._get_os_path(path)
482 os_path = self._get_os_path(path)
480 self.log.debug("Saving %s", os_path)
483 self.log.debug("Saving %s", os_path)
481 try:
484 try:
482 if model['type'] == 'notebook':
485 if model['type'] == 'notebook':
483 self._save_notebook(os_path, model, path)
486 self._save_notebook(os_path, model, path)
484 elif model['type'] == 'file':
487 elif model['type'] == 'file':
485 self._save_file(os_path, model, path)
488 self._save_file(os_path, model, path)
486 elif model['type'] == 'directory':
489 elif model['type'] == 'directory':
487 self._save_directory(os_path, model, path)
490 self._save_directory(os_path, model, path)
488 else:
491 else:
489 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
492 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
490 except web.HTTPError:
493 except web.HTTPError:
491 raise
494 raise
492 except Exception as e:
495 except Exception as e:
493 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
496 self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True)
494 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
497 raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e))
495
498
496 validation_message = None
499 validation_message = None
497 if model['type'] == 'notebook':
500 if model['type'] == 'notebook':
498 self.validate_notebook_model(model)
501 self.validate_notebook_model(model)
499 validation_message = model.get('message', None)
502 validation_message = model.get('message', None)
500
503
501 model = self.get(path, content=False)
504 model = self.get(path, content=False)
502 if validation_message:
505 if validation_message:
503 model['message'] = validation_message
506 model['message'] = validation_message
504
507
505 self.run_post_save_hook(model=model, os_path=os_path)
508 self.run_post_save_hook(model=model, os_path=os_path)
506
509
507 return model
510 return model
508
511
509 def update(self, model, path):
512 def update(self, model, path):
510 """Update the file's path
513 """Update the file's path
511
514
512 For use in PATCH requests, to enable renaming a file without
515 For use in PATCH requests, to enable renaming a file without
513 re-uploading its contents. Only used for renaming at the moment.
516 re-uploading its contents. Only used for renaming at the moment.
514 """
517 """
515 path = path.strip('/')
518 path = path.strip('/')
516 new_path = model.get('path', path).strip('/')
519 new_path = model.get('path', path).strip('/')
517 if path != new_path:
520 if path != new_path:
518 self.rename(path, new_path)
521 self.rename(path, new_path)
519 model = self.get(new_path, content=False)
522 model = self.get(new_path, content=False)
520 return model
523 return model
521
524
522 def delete(self, path):
525 def delete(self, path):
523 """Delete file at path."""
526 """Delete file at path."""
524 path = path.strip('/')
527 path = path.strip('/')
525 os_path = self._get_os_path(path)
528 os_path = self._get_os_path(path)
526 rm = os.unlink
529 rm = os.unlink
527 if os.path.isdir(os_path):
530 if os.path.isdir(os_path):
528 listing = os.listdir(os_path)
531 listing = os.listdir(os_path)
529 # don't delete non-empty directories (checkpoints dir doesn't count)
532 # don't delete non-empty directories (checkpoints dir doesn't count)
530 if listing and listing != [self.checkpoint_dir]:
533 if listing and listing != [self.checkpoint_dir]:
531 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
534 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
532 elif not os.path.isfile(os_path):
535 elif not os.path.isfile(os_path):
533 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
536 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
534
537
535 # clear checkpoints
538 # clear checkpoints
536 for checkpoint in self.list_checkpoints(path):
539 for checkpoint in self.list_checkpoints(path):
537 checkpoint_id = checkpoint['id']
540 checkpoint_id = checkpoint['id']
538 cp_path = self.get_checkpoint_path(checkpoint_id, path)
541 cp_path = self.get_checkpoint_path(checkpoint_id, path)
539 if os.path.isfile(cp_path):
542 if os.path.isfile(cp_path):
540 self.log.debug("Unlinking checkpoint %s", cp_path)
543 self.log.debug("Unlinking checkpoint %s", cp_path)
541 with self.perm_to_403():
544 with self.perm_to_403():
542 rm(cp_path)
545 rm(cp_path)
543
546
544 if os.path.isdir(os_path):
547 if os.path.isdir(os_path):
545 self.log.debug("Removing directory %s", os_path)
548 self.log.debug("Removing directory %s", os_path)
546 with self.perm_to_403():
549 with self.perm_to_403():
547 shutil.rmtree(os_path)
550 shutil.rmtree(os_path)
548 else:
551 else:
549 self.log.debug("Unlinking file %s", os_path)
552 self.log.debug("Unlinking file %s", os_path)
550 with self.perm_to_403():
553 with self.perm_to_403():
551 rm(os_path)
554 rm(os_path)
552
555
553 def rename(self, old_path, new_path):
556 def rename(self, old_path, new_path):
554 """Rename a file."""
557 """Rename a file."""
555 old_path = old_path.strip('/')
558 old_path = old_path.strip('/')
556 new_path = new_path.strip('/')
559 new_path = new_path.strip('/')
557 if new_path == old_path:
560 if new_path == old_path:
558 return
561 return
559
562
560 new_os_path = self._get_os_path(new_path)
563 new_os_path = self._get_os_path(new_path)
561 old_os_path = self._get_os_path(old_path)
564 old_os_path = self._get_os_path(old_path)
562
565
563 # Should we proceed with the move?
566 # Should we proceed with the move?
564 if os.path.exists(new_os_path):
567 if os.path.exists(new_os_path):
565 raise web.HTTPError(409, u'File already exists: %s' % new_path)
568 raise web.HTTPError(409, u'File already exists: %s' % new_path)
566
569
567 # Move the file
570 # Move the file
568 try:
571 try:
569 with self.perm_to_403():
572 with self.perm_to_403():
570 shutil.move(old_os_path, new_os_path)
573 shutil.move(old_os_path, new_os_path)
571 except web.HTTPError:
574 except web.HTTPError:
572 raise
575 raise
573 except Exception as e:
576 except Exception as e:
574 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
577 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
575
578
576 # Move the checkpoints
579 # Move the checkpoints
577 old_checkpoints = self.list_checkpoints(old_path)
580 old_checkpoints = self.list_checkpoints(old_path)
578 for cp in old_checkpoints:
581 for cp in old_checkpoints:
579 checkpoint_id = cp['id']
582 checkpoint_id = cp['id']
580 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
583 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
581 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
584 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
582 if os.path.isfile(old_cp_path):
585 if os.path.isfile(old_cp_path):
583 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
586 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
584 with self.perm_to_403():
587 with self.perm_to_403():
585 shutil.move(old_cp_path, new_cp_path)
588 shutil.move(old_cp_path, new_cp_path)
586
589
587 # Checkpoint-related utilities
590 # Checkpoint-related utilities
588
591
589 def get_checkpoint_path(self, checkpoint_id, path):
592 def get_checkpoint_path(self, checkpoint_id, path):
590 """find the path to a checkpoint"""
593 """find the path to a checkpoint"""
591 path = path.strip('/')
594 path = path.strip('/')
592 parent, name = ('/' + path).rsplit('/', 1)
595 parent, name = ('/' + path).rsplit('/', 1)
593 parent = parent.strip('/')
596 parent = parent.strip('/')
594 basename, ext = os.path.splitext(name)
597 basename, ext = os.path.splitext(name)
595 filename = u"{name}-{checkpoint_id}{ext}".format(
598 filename = u"{name}-{checkpoint_id}{ext}".format(
596 name=basename,
599 name=basename,
597 checkpoint_id=checkpoint_id,
600 checkpoint_id=checkpoint_id,
598 ext=ext,
601 ext=ext,
599 )
602 )
600 os_path = self._get_os_path(path=parent)
603 os_path = self._get_os_path(path=parent)
601 cp_dir = os.path.join(os_path, self.checkpoint_dir)
604 cp_dir = os.path.join(os_path, self.checkpoint_dir)
602 with self.perm_to_403():
605 with self.perm_to_403():
603 ensure_dir_exists(cp_dir)
606 ensure_dir_exists(cp_dir)
604 cp_path = os.path.join(cp_dir, filename)
607 cp_path = os.path.join(cp_dir, filename)
605 return cp_path
608 return cp_path
606
609
607 def get_checkpoint_model(self, checkpoint_id, path):
610 def get_checkpoint_model(self, checkpoint_id, path):
608 """construct the info dict for a given checkpoint"""
611 """construct the info dict for a given checkpoint"""
609 path = path.strip('/')
612 path = path.strip('/')
610 cp_path = self.get_checkpoint_path(checkpoint_id, path)
613 cp_path = self.get_checkpoint_path(checkpoint_id, path)
611 stats = os.stat(cp_path)
614 stats = os.stat(cp_path)
612 last_modified = tz.utcfromtimestamp(stats.st_mtime)
615 last_modified = tz.utcfromtimestamp(stats.st_mtime)
613 info = dict(
616 info = dict(
614 id = checkpoint_id,
617 id = checkpoint_id,
615 last_modified = last_modified,
618 last_modified = last_modified,
616 )
619 )
617 return info
620 return info
618
621
619 # public checkpoint API
622 # public checkpoint API
620
623
621 def create_checkpoint(self, path):
624 def create_checkpoint(self, path):
622 """Create a checkpoint from the current state of a file"""
625 """Create a checkpoint from the current state of a file"""
623 path = path.strip('/')
626 path = path.strip('/')
624 if not self.file_exists(path):
627 if not self.file_exists(path):
625 raise web.HTTPError(404)
628 raise web.HTTPError(404)
626 src_path = self._get_os_path(path)
629 src_path = self._get_os_path(path)
627 # only the one checkpoint ID:
630 # only the one checkpoint ID:
628 checkpoint_id = u"checkpoint"
631 checkpoint_id = u"checkpoint"
629 cp_path = self.get_checkpoint_path(checkpoint_id, path)
632 cp_path = self.get_checkpoint_path(checkpoint_id, path)
630 self.log.debug("creating checkpoint for %s", path)
633 self.log.debug("creating checkpoint for %s", path)
631 with self.perm_to_403():
634 with self.perm_to_403():
632 self._copy(src_path, cp_path)
635 self._copy(src_path, cp_path)
633
636
634 # return the checkpoint info
637 # return the checkpoint info
635 return self.get_checkpoint_model(checkpoint_id, path)
638 return self.get_checkpoint_model(checkpoint_id, path)
636
639
637 def list_checkpoints(self, path):
640 def list_checkpoints(self, path):
638 """list the checkpoints for a given file
641 """list the checkpoints for a given file
639
642
640 This contents manager currently only supports one checkpoint per file.
643 This contents manager currently only supports one checkpoint per file.
641 """
644 """
642 path = path.strip('/')
645 path = path.strip('/')
643 checkpoint_id = "checkpoint"
646 checkpoint_id = "checkpoint"
644 os_path = self.get_checkpoint_path(checkpoint_id, path)
647 os_path = self.get_checkpoint_path(checkpoint_id, path)
645 if not os.path.exists(os_path):
648 if not os.path.exists(os_path):
646 return []
649 return []
647 else:
650 else:
648 return [self.get_checkpoint_model(checkpoint_id, path)]
651 return [self.get_checkpoint_model(checkpoint_id, path)]
649
652
650
653
651 def restore_checkpoint(self, checkpoint_id, path):
654 def restore_checkpoint(self, checkpoint_id, path):
652 """restore a file to a checkpointed state"""
655 """restore a file to a checkpointed state"""
653 path = path.strip('/')
656 path = path.strip('/')
654 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
657 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
655 nb_path = self._get_os_path(path)
658 nb_path = self._get_os_path(path)
656 cp_path = self.get_checkpoint_path(checkpoint_id, path)
659 cp_path = self.get_checkpoint_path(checkpoint_id, path)
657 if not os.path.isfile(cp_path):
660 if not os.path.isfile(cp_path):
658 self.log.debug("checkpoint file does not exist: %s", cp_path)
661 self.log.debug("checkpoint file does not exist: %s", cp_path)
659 raise web.HTTPError(404,
662 raise web.HTTPError(404,
660 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
663 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
661 )
664 )
662 # ensure notebook is readable (never restore from an unreadable notebook)
665 # ensure notebook is readable (never restore from an unreadable notebook)
663 if cp_path.endswith('.ipynb'):
666 if cp_path.endswith('.ipynb'):
664 with self.open(cp_path, 'r', encoding='utf-8') as f:
667 with self.open(cp_path, 'r', encoding='utf-8') as f:
665 nbformat.read(f, as_version=4)
668 nbformat.read(f, as_version=4)
666 self.log.debug("copying %s -> %s", cp_path, nb_path)
669 self.log.debug("copying %s -> %s", cp_path, nb_path)
667 with self.perm_to_403():
670 with self.perm_to_403():
668 self._copy(cp_path, nb_path)
671 self._copy(cp_path, nb_path)
669
672
670 def delete_checkpoint(self, checkpoint_id, path):
673 def delete_checkpoint(self, checkpoint_id, path):
671 """delete a file's checkpoint"""
674 """delete a file's checkpoint"""
672 path = path.strip('/')
675 path = path.strip('/')
673 cp_path = self.get_checkpoint_path(checkpoint_id, path)
676 cp_path = self.get_checkpoint_path(checkpoint_id, path)
674 if not os.path.isfile(cp_path):
677 if not os.path.isfile(cp_path):
675 raise web.HTTPError(404,
678 raise web.HTTPError(404,
676 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
679 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
677 )
680 )
678 self.log.debug("unlinking %s", cp_path)
681 self.log.debug("unlinking %s", cp_path)
679 os.unlink(cp_path)
682 os.unlink(cp_path)
680
683
681 def info_string(self):
684 def info_string(self):
682 return "Serving notebooks from local directory: %s" % self.root_dir
685 return "Serving notebooks from local directory: %s" % self.root_dir
683
686
684 def get_kernel_path(self, path, model=None):
687 def get_kernel_path(self, path, model=None):
685 """Return the initial API path of a kernel associated with a given notebook"""
688 """Return the initial API path of a kernel associated with a given notebook"""
686 if '/' in path:
689 if '/' in path:
687 parent_dir = path.rsplit('/', 1)[0]
690 parent_dir = path.rsplit('/', 1)[0]
688 else:
691 else:
689 parent_dir = ''
692 parent_dir = ''
690 return parent_dir
693 return parent_dir
General Comments 0
You need to be logged in to leave comments. Login now