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