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