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