##// END OF EJS Templates
teach contents service about non-notebook files
MinRK -
Show More
@@ -1,427 +1,429 b''
1 1 """Base Tornado handlers for the notebook server."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import functools
7 7 import json
8 8 import logging
9 9 import os
10 10 import re
11 11 import sys
12 12 import traceback
13 13 try:
14 14 # py3
15 15 from http.client import responses
16 16 except ImportError:
17 17 from httplib import responses
18 18
19 19 from jinja2 import TemplateNotFound
20 20 from tornado import web
21 21
22 22 try:
23 23 from tornado.log import app_log
24 24 except ImportError:
25 25 app_log = logging.getLogger()
26 26
27 27 from IPython.config import Application
28 28 from IPython.utils.path import filefind
29 29 from IPython.utils.py3compat import string_types
30 30 from IPython.html.utils import is_hidden
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Top-level handlers
34 34 #-----------------------------------------------------------------------------
35 35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36 36
37 37 class AuthenticatedHandler(web.RequestHandler):
38 38 """A RequestHandler with an authenticated user."""
39 39
40 40 def set_default_headers(self):
41 41 headers = self.settings.get('headers', {})
42 42
43 43 if "X-Frame-Options" not in headers:
44 44 headers["X-Frame-Options"] = "SAMEORIGIN"
45 45
46 46 for header_name,value in headers.items() :
47 47 try:
48 48 self.set_header(header_name, value)
49 49 except Exception:
50 50 # tornado raise Exception (not a subclass)
51 51 # if method is unsupported (websocket and Access-Control-Allow-Origin
52 52 # for example, so just ignore)
53 53 pass
54 54
55 55 def clear_login_cookie(self):
56 56 self.clear_cookie(self.cookie_name)
57 57
58 58 def get_current_user(self):
59 59 user_id = self.get_secure_cookie(self.cookie_name)
60 60 # For now the user_id should not return empty, but it could eventually
61 61 if user_id == '':
62 62 user_id = 'anonymous'
63 63 if user_id is None:
64 64 # prevent extra Invalid cookie sig warnings:
65 65 self.clear_login_cookie()
66 66 if not self.login_available:
67 67 user_id = 'anonymous'
68 68 return user_id
69 69
70 70 @property
71 71 def cookie_name(self):
72 72 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
73 73 self.request.host
74 74 ))
75 75 return self.settings.get('cookie_name', default_cookie_name)
76 76
77 77 @property
78 78 def password(self):
79 79 """our password"""
80 80 return self.settings.get('password', '')
81 81
82 82 @property
83 83 def logged_in(self):
84 84 """Is a user currently logged in?
85 85
86 86 """
87 87 user = self.get_current_user()
88 88 return (user and not user == 'anonymous')
89 89
90 90 @property
91 91 def login_available(self):
92 92 """May a user proceed to log in?
93 93
94 94 This returns True if login capability is available, irrespective of
95 95 whether the user is already logged in or not.
96 96
97 97 """
98 98 return bool(self.settings.get('password', ''))
99 99
100 100
101 101 class IPythonHandler(AuthenticatedHandler):
102 102 """IPython-specific extensions to authenticated handling
103 103
104 104 Mostly property shortcuts to IPython-specific settings.
105 105 """
106 106
107 107 @property
108 108 def config(self):
109 109 return self.settings.get('config', None)
110 110
111 111 @property
112 112 def log(self):
113 113 """use the IPython log by default, falling back on tornado's logger"""
114 114 if Application.initialized():
115 115 return Application.instance().log
116 116 else:
117 117 return app_log
118 118
119 119 #---------------------------------------------------------------
120 120 # URLs
121 121 #---------------------------------------------------------------
122 122
123 123 @property
124 124 def mathjax_url(self):
125 125 return self.settings.get('mathjax_url', '')
126 126
127 127 @property
128 128 def base_url(self):
129 129 return self.settings.get('base_url', '/')
130 130
131 131 @property
132 132 def ws_url(self):
133 133 return self.settings.get('websocket_url', '')
134 134
135 135 #---------------------------------------------------------------
136 136 # Manager objects
137 137 #---------------------------------------------------------------
138 138
139 139 @property
140 140 def kernel_manager(self):
141 141 return self.settings['kernel_manager']
142 142
143 143 @property
144 144 def contents_manager(self):
145 145 return self.settings['contents_manager']
146 146
147 147 @property
148 148 def cluster_manager(self):
149 149 return self.settings['cluster_manager']
150 150
151 151 @property
152 152 def session_manager(self):
153 153 return self.settings['session_manager']
154 154
155 155 @property
156 156 def kernel_spec_manager(self):
157 157 return self.settings['kernel_spec_manager']
158 158
159 159 @property
160 160 def project_dir(self):
161 161 return getattr(self.contents_manager, 'root_dir', '/')
162 162
163 163 #---------------------------------------------------------------
164 164 # CORS
165 165 #---------------------------------------------------------------
166 166
167 167 @property
168 168 def allow_origin(self):
169 169 """Normal Access-Control-Allow-Origin"""
170 170 return self.settings.get('allow_origin', '')
171 171
172 172 @property
173 173 def allow_origin_pat(self):
174 174 """Regular expression version of allow_origin"""
175 175 return self.settings.get('allow_origin_pat', None)
176 176
177 177 @property
178 178 def allow_credentials(self):
179 179 """Whether to set Access-Control-Allow-Credentials"""
180 180 return self.settings.get('allow_credentials', False)
181 181
182 182 def set_default_headers(self):
183 183 """Add CORS headers, if defined"""
184 184 super(IPythonHandler, self).set_default_headers()
185 185 if self.allow_origin:
186 186 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
187 187 elif self.allow_origin_pat:
188 188 origin = self.get_origin()
189 189 if origin and self.allow_origin_pat.match(origin):
190 190 self.set_header("Access-Control-Allow-Origin", origin)
191 191 if self.allow_credentials:
192 192 self.set_header("Access-Control-Allow-Credentials", 'true')
193 193
194 194 def get_origin(self):
195 195 # Handle WebSocket Origin naming convention differences
196 196 # The difference between version 8 and 13 is that in 8 the
197 197 # client sends a "Sec-Websocket-Origin" header and in 13 it's
198 198 # simply "Origin".
199 199 if "Origin" in self.request.headers:
200 200 origin = self.request.headers.get("Origin")
201 201 else:
202 202 origin = self.request.headers.get("Sec-Websocket-Origin", None)
203 203 return origin
204 204
205 205 #---------------------------------------------------------------
206 206 # template rendering
207 207 #---------------------------------------------------------------
208 208
209 209 def get_template(self, name):
210 210 """Return the jinja template object for a given name"""
211 211 return self.settings['jinja2_env'].get_template(name)
212 212
213 213 def render_template(self, name, **ns):
214 214 ns.update(self.template_namespace)
215 215 template = self.get_template(name)
216 216 return template.render(**ns)
217 217
218 218 @property
219 219 def template_namespace(self):
220 220 return dict(
221 221 base_url=self.base_url,
222 222 ws_url=self.ws_url,
223 223 logged_in=self.logged_in,
224 224 login_available=self.login_available,
225 225 static_url=self.static_url,
226 226 )
227 227
228 228 def get_json_body(self):
229 229 """Return the body of the request as JSON data."""
230 230 if not self.request.body:
231 231 return None
232 232 # Do we need to call body.decode('utf-8') here?
233 233 body = self.request.body.strip().decode(u'utf-8')
234 234 try:
235 235 model = json.loads(body)
236 236 except Exception:
237 237 self.log.debug("Bad JSON: %r", body)
238 238 self.log.error("Couldn't parse JSON", exc_info=True)
239 239 raise web.HTTPError(400, u'Invalid JSON in body of request')
240 240 return model
241 241
242 242 def get_error_html(self, status_code, **kwargs):
243 243 """render custom error pages"""
244 244 exception = kwargs.get('exception')
245 245 message = ''
246 246 status_message = responses.get(status_code, 'Unknown HTTP Error')
247 247 if exception:
248 248 # get the custom message, if defined
249 249 try:
250 250 message = exception.log_message % exception.args
251 251 except Exception:
252 252 pass
253 253
254 254 # construct the custom reason, if defined
255 255 reason = getattr(exception, 'reason', '')
256 256 if reason:
257 257 status_message = reason
258 258
259 259 # build template namespace
260 260 ns = dict(
261 261 status_code=status_code,
262 262 status_message=status_message,
263 263 message=message,
264 264 exception=exception,
265 265 )
266 266
267 267 # render the template
268 268 try:
269 269 html = self.render_template('%s.html' % status_code, **ns)
270 270 except TemplateNotFound:
271 271 self.log.debug("No template for %d", status_code)
272 272 html = self.render_template('error.html', **ns)
273 273 return html
274 274
275 275
276 276 class Template404(IPythonHandler):
277 277 """Render our 404 template"""
278 278 def prepare(self):
279 279 raise web.HTTPError(404)
280 280
281 281
282 282 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
283 283 """static files should only be accessible when logged in"""
284 284
285 285 @web.authenticated
286 286 def get(self, path):
287 287 if os.path.splitext(path)[1] == '.ipynb':
288 288 name = os.path.basename(path)
289 289 self.set_header('Content-Type', 'application/json')
290 290 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
291 291
292 292 return web.StaticFileHandler.get(self, path)
293 293
294 294 def compute_etag(self):
295 295 return None
296 296
297 297 def validate_absolute_path(self, root, absolute_path):
298 298 """Validate and return the absolute path.
299 299
300 300 Requires tornado 3.1
301 301
302 302 Adding to tornado's own handling, forbids the serving of hidden files.
303 303 """
304 304 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
305 305 abs_root = os.path.abspath(root)
306 306 if is_hidden(abs_path, abs_root):
307 307 self.log.info("Refusing to serve hidden file, via 404 Error")
308 308 raise web.HTTPError(404)
309 309 return abs_path
310 310
311 311
312 312 def json_errors(method):
313 313 """Decorate methods with this to return GitHub style JSON errors.
314 314
315 315 This should be used on any JSON API on any handler method that can raise HTTPErrors.
316 316
317 317 This will grab the latest HTTPError exception using sys.exc_info
318 318 and then:
319 319
320 320 1. Set the HTTP status code based on the HTTPError
321 321 2. Create and return a JSON body with a message field describing
322 322 the error in a human readable form.
323 323 """
324 324 @functools.wraps(method)
325 325 def wrapper(self, *args, **kwargs):
326 326 try:
327 327 result = method(self, *args, **kwargs)
328 328 except web.HTTPError as e:
329 329 status = e.status_code
330 330 message = e.log_message
331 331 self.log.warn(message)
332 332 self.set_status(e.status_code)
333 333 self.finish(json.dumps(dict(message=message)))
334 334 except Exception:
335 335 self.log.error("Unhandled error in API request", exc_info=True)
336 336 status = 500
337 337 message = "Unknown server error"
338 338 t, value, tb = sys.exc_info()
339 339 self.set_status(status)
340 340 tb_text = ''.join(traceback.format_exception(t, value, tb))
341 341 reply = dict(message=message, traceback=tb_text)
342 342 self.finish(json.dumps(reply))
343 343 else:
344 344 return result
345 345 return wrapper
346 346
347 347
348 348
349 349 #-----------------------------------------------------------------------------
350 350 # File handler
351 351 #-----------------------------------------------------------------------------
352 352
353 353 # to minimize subclass changes:
354 354 HTTPError = web.HTTPError
355 355
356 356 class FileFindHandler(web.StaticFileHandler):
357 357 """subclass of StaticFileHandler for serving files from a search path"""
358 358
359 359 # cache search results, don't search for files more than once
360 360 _static_paths = {}
361 361
362 362 def initialize(self, path, default_filename=None):
363 363 if isinstance(path, string_types):
364 364 path = [path]
365 365
366 366 self.root = tuple(
367 367 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
368 368 )
369 369 self.default_filename = default_filename
370 370
371 371 def compute_etag(self):
372 372 return None
373 373
374 374 @classmethod
375 375 def get_absolute_path(cls, roots, path):
376 376 """locate a file to serve on our static file search path"""
377 377 with cls._lock:
378 378 if path in cls._static_paths:
379 379 return cls._static_paths[path]
380 380 try:
381 381 abspath = os.path.abspath(filefind(path, roots))
382 382 except IOError:
383 383 # IOError means not found
384 384 return ''
385 385
386 386 cls._static_paths[path] = abspath
387 387 return abspath
388 388
389 389 def validate_absolute_path(self, root, absolute_path):
390 390 """check if the file should be served (raises 404, 403, etc.)"""
391 391 if absolute_path == '':
392 392 raise web.HTTPError(404)
393 393
394 394 for root in self.root:
395 395 if (absolute_path + os.sep).startswith(root):
396 396 break
397 397
398 398 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
399 399
400 400
401 401 class TrailingSlashHandler(web.RequestHandler):
402 402 """Simple redirect handler that strips trailing slashes
403 403
404 404 This should be the first, highest priority handler.
405 405 """
406 406
407 407 SUPPORTED_METHODS = ['GET']
408 408
409 409 def get(self):
410 410 self.redirect(self.request.uri.rstrip('/'))
411 411
412 412 #-----------------------------------------------------------------------------
413 413 # URL pattern fragments for re-use
414 414 #-----------------------------------------------------------------------------
415 415
416 416 path_regex = r"(?P<path>(?:/.*)*)"
417 417 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
418 418 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
419 file_name_regex = r"(?P<name>[^/]+)"
420 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
419 421
420 422 #-----------------------------------------------------------------------------
421 423 # URL to handler mappings
422 424 #-----------------------------------------------------------------------------
423 425
424 426
425 427 default_handlers = [
426 428 (r".*/", TrailingSlashHandler)
427 429 ]
@@ -1,137 +1,142 b''
1 """Tornado handlers for nbconvert."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
1 6 import io
2 7 import os
3 8 import zipfile
4 9
5 10 from tornado import web
6 11
7 12 from ..base.handlers import IPythonHandler, notebook_path_regex
8 13 from IPython.nbformat.current import to_notebook_json
9 14
10 15 from IPython.utils.py3compat import cast_bytes
11 16
12 17 def find_resource_files(output_files_dir):
13 18 files = []
14 19 for dirpath, dirnames, filenames in os.walk(output_files_dir):
15 20 files.extend([os.path.join(dirpath, f) for f in filenames])
16 21 return files
17 22
18 23 def respond_zip(handler, name, output, resources):
19 24 """Zip up the output and resource files and respond with the zip file.
20 25
21 26 Returns True if it has served a zip file, False if there are no resource
22 27 files, in which case we serve the plain output file.
23 28 """
24 29 # Check if we have resource files we need to zip
25 30 output_files = resources.get('outputs', None)
26 31 if not output_files:
27 32 return False
28 33
29 34 # Headers
30 35 zip_filename = os.path.splitext(name)[0] + '.zip'
31 36 handler.set_header('Content-Disposition',
32 37 'attachment; filename="%s"' % zip_filename)
33 38 handler.set_header('Content-Type', 'application/zip')
34 39
35 40 # Prepare the zip file
36 41 buffer = io.BytesIO()
37 42 zipf = zipfile.ZipFile(buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
38 43 output_filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
39 44 zipf.writestr(output_filename, cast_bytes(output, 'utf-8'))
40 45 for filename, data in output_files.items():
41 46 zipf.writestr(os.path.basename(filename), data)
42 47 zipf.close()
43 48
44 49 handler.finish(buffer.getvalue())
45 50 return True
46 51
47 52 def get_exporter(format, **kwargs):
48 53 """get an exporter, raising appropriate errors"""
49 54 # if this fails, will raise 500
50 55 try:
51 56 from IPython.nbconvert.exporters.export import exporter_map
52 57 except ImportError as e:
53 58 raise web.HTTPError(500, "Could not import nbconvert: %s" % e)
54 59
55 60 try:
56 61 Exporter = exporter_map[format]
57 62 except KeyError:
58 63 # should this be 400?
59 64 raise web.HTTPError(404, u"No exporter for format: %s" % format)
60 65
61 66 try:
62 67 return Exporter(**kwargs)
63 68 except Exception as e:
64 69 raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
65 70
66 71 class NbconvertFileHandler(IPythonHandler):
67 72
68 73 SUPPORTED_METHODS = ('GET',)
69 74
70 75 @web.authenticated
71 76 def get(self, format, path='', name=None):
72 77
73 78 exporter = get_exporter(format, config=self.config, log=self.log)
74 79
75 80 path = path.strip('/')
76 model = self.contents_manager.get(name=name, path=path)
81 model = self.contents_manager.get_model(name=name, path=path)
77 82
78 83 self.set_header('Last-Modified', model['last_modified'])
79 84
80 85 try:
81 86 output, resources = exporter.from_notebook_node(model['content'])
82 87 except Exception as e:
83 88 raise web.HTTPError(500, "nbconvert failed: %s" % e)
84 89
85 90 if respond_zip(self, name, output, resources):
86 91 return
87 92
88 93 # Force download if requested
89 94 if self.get_argument('download', 'false').lower() == 'true':
90 95 filename = os.path.splitext(name)[0] + '.' + resources['output_extension']
91 96 self.set_header('Content-Disposition',
92 97 'attachment; filename="%s"' % filename)
93 98
94 99 # MIME type
95 100 if exporter.output_mimetype:
96 101 self.set_header('Content-Type',
97 102 '%s; charset=utf-8' % exporter.output_mimetype)
98 103
99 104 self.finish(output)
100 105
101 106 class NbconvertPostHandler(IPythonHandler):
102 107 SUPPORTED_METHODS = ('POST',)
103 108
104 109 @web.authenticated
105 110 def post(self, format):
106 111 exporter = get_exporter(format, config=self.config)
107 112
108 113 model = self.get_json_body()
109 114 nbnode = to_notebook_json(model['content'])
110 115
111 116 try:
112 117 output, resources = exporter.from_notebook_node(nbnode)
113 118 except Exception as e:
114 119 raise web.HTTPError(500, "nbconvert failed: %s" % e)
115 120
116 121 if respond_zip(self, nbnode.metadata.name, output, resources):
117 122 return
118 123
119 124 # MIME type
120 125 if exporter.output_mimetype:
121 126 self.set_header('Content-Type',
122 127 '%s; charset=utf-8' % exporter.output_mimetype)
123 128
124 129 self.finish(output)
125 130
126 131 #-----------------------------------------------------------------------------
127 132 # URL to handler mappings
128 133 #-----------------------------------------------------------------------------
129 134
130 135 _format_regex = r"(?P<format>\w+)"
131 136
132 137
133 138 default_handlers = [
134 139 (r"/nbconvert/%s%s" % (_format_regex, notebook_path_regex),
135 140 NbconvertFileHandler),
136 141 (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler),
137 142 ]
@@ -1,437 +1,461 b''
1 1 """A contents manager that uses the local file system for storage."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 import base64
6 7 import io
7 8 import os
8 9 import glob
9 10 import shutil
10 11
11 12 from tornado import web
12 13
13 14 from .manager import ContentsManager
14 15 from IPython.nbformat import current
15 16 from IPython.utils.path import ensure_dir_exists
16 17 from IPython.utils.traitlets import Unicode, Bool, TraitError
17 18 from IPython.utils.py3compat import getcwd
18 19 from IPython.utils import tz
19 20 from IPython.html.utils import is_hidden, to_os_path
20 21
21 22 def sort_key(item):
22 23 """Case-insensitive sorting."""
23 24 return item['name'].lower()
24 25
25 26
26 27 class FileContentsManager(ContentsManager):
27 28
28 29 root_dir = Unicode(getcwd(), config=True)
29 30
30 31 def _root_dir_changed(self, name, old, new):
31 32 """Do a bit of validation of the root_dir."""
32 33 if not os.path.isabs(new):
33 34 # If we receive a non-absolute path, make it absolute.
34 35 self.root_dir = os.path.abspath(new)
35 36 return
36 37 if not os.path.exists(new) or not os.path.isdir(new):
37 38 raise TraitError("%r is not a directory" % new)
38 39
39 40 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
40 41 help="""The directory name in which to keep notebook checkpoints
41 42
42 43 This is a path relative to the notebook's own directory.
43 44
44 45 By default, it is .ipynb_checkpoints
45 46 """
46 47 )
47 48
48 49 def _copy(self, src, dest):
49 50 """copy src to dest
50 51
51 52 like shutil.copy2, but log errors in copystat
52 53 """
53 54 shutil.copyfile(src, dest)
54 55 try:
55 56 shutil.copystat(src, dest)
56 57 except OSError as e:
57 58 self.log.debug("copystat on %s failed", dest, exc_info=True)
58 59
59 def get_names(self, path=''):
60 """List all filenames in the path (relative to root_dir)."""
61 path = path.strip('/')
62 if not os.path.isdir(self._get_os_path(path=path)):
63 raise web.HTTPError(404, 'Directory not found: ' + path)
64 names = glob.glob(self._get_os_path('*', path))
65 names = [ os.path.basename(name) for name in names if os.path.isfile(name)]
66 return names
60 def _get_os_path(self, name=None, path=''):
61 """Given a filename and a URL path, return its file system
62 path.
63
64 Parameters
65 ----------
66 name : string
67 A filename
68 path : string
69 The relative URL path (with '/' as separator) to the named
70 file.
71
72 Returns
73 -------
74 path : string
75 API path to be evaluated relative to root_dir.
76 """
77 if name is not None:
78 path = path + '/' + name
79 return to_os_path(path, self.root_dir)
67 80
68 81 def path_exists(self, path):
69 """Does the API-style path (directory) actually exist?
82 """Does the API-style path refer to an extant directory?
70 83
71 84 Parameters
72 85 ----------
73 86 path : string
74 87 The path to check. This is an API path (`/` separated,
75 88 relative to root_dir).
76 89
77 90 Returns
78 91 -------
79 92 exists : bool
80 93 Whether the path is indeed a directory.
81 94 """
82 95 path = path.strip('/')
83 96 os_path = self._get_os_path(path=path)
84 97 return os.path.isdir(os_path)
85 98
86 99 def is_hidden(self, path):
87 100 """Does the API style path correspond to a hidden directory or file?
88 101
89 102 Parameters
90 103 ----------
91 104 path : string
92 105 The path to check. This is an API path (`/` separated,
93 106 relative to root_dir).
94 107
95 108 Returns
96 109 -------
97 110 exists : bool
98 111 Whether the path is hidden.
99 112
100 113 """
101 114 path = path.strip('/')
102 115 os_path = self._get_os_path(path=path)
103 116 return is_hidden(os_path, self.root_dir)
104 117
105 def _get_os_path(self, name=None, path=''):
106 """Given a filename and a URL path, return its file system
107 path.
118 def file_exists(self, name, path=''):
119 """Returns True if the file exists, else returns False.
108 120
109 121 Parameters
110 122 ----------
111 123 name : string
112 A filename
124 The name of the file you are checking.
113 125 path : string
114 The relative URL path (with '/' as separator) to the named
115 file.
126 The relative path to the file's directory (with '/' as separator)
116 127
117 128 Returns
118 129 -------
119 path : string
120 API path to be evaluated relative to root_dir.
130 bool
121 131 """
122 if name is not None:
123 path = path + '/' + name
124 return to_os_path(path, self.root_dir)
132 path = path.strip('/')
133 nbpath = self._get_os_path(name, path=path)
134 return os.path.isfile(nbpath)
125 135
126 def file_exists(self, name, path=''):
127 """Returns a True if the file exists, else returns False.
136 def exists(self, name=None, path=''):
137 """Returns True if the path [and name] exists, else returns False.
128 138
129 139 Parameters
130 140 ----------
131 141 name : string
132 142 The name of the file you are checking.
133 143 path : string
134 144 The relative path to the file's directory (with '/' as separator)
135 145
136 146 Returns
137 147 -------
138 148 bool
139 149 """
140 150 path = path.strip('/')
141 nbpath = self._get_os_path(name, path=path)
142 return os.path.isfile(nbpath)
151 os_path = self._get_os_path(name, path=path)
152 return os.path.exists(os_path)
143 153
144 # TODO: Remove this after we create the contents web service and directories are
145 # no longer listed by the notebook web service.
146 def list_dirs(self, path):
147 """List the directories for a given API style path."""
148 path = path.strip('/')
149 os_path = self._get_os_path('', path)
150 if not os.path.isdir(os_path):
151 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
152 elif is_hidden(os_path, self.root_dir):
153 self.log.info("Refusing to serve hidden directory, via 404 Error")
154 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
155 dir_names = os.listdir(os_path)
156 dirs = []
157 for name in dir_names:
158 os_path = self._get_os_path(name, path)
159 if os.path.isdir(os_path) and not is_hidden(os_path, self.root_dir)\
160 and self.should_list(name):
161 try:
162 model = self.get_dir_model(name, path)
163 except IOError:
164 pass
165 dirs.append(model)
166 dirs = sorted(dirs, key=sort_key)
167 return dirs
168
169 # TODO: Remove this after we create the contents web service and directories are
170 # no longer listed by the notebook web service.
171 def get_dir_model(self, name, path=''):
172 """Get the directory model given a directory name and its API style path"""
173 path = path.strip('/')
154 def _base_model(self, name, path=''):
155 """Build the common base of a contents model"""
174 156 os_path = self._get_os_path(name, path)
175 if not os.path.isdir(os_path):
176 raise IOError('directory does not exist: %r' % os_path)
177 157 info = os.stat(os_path)
178 158 last_modified = tz.utcfromtimestamp(info.st_mtime)
179 159 created = tz.utcfromtimestamp(info.st_ctime)
180 160 # Create the notebook model.
181 model ={}
161 model = {}
182 162 model['name'] = name
183 163 model['path'] = path
184 164 model['last_modified'] = last_modified
185 165 model['created'] = created
166 model['content'] = None
167 model['format'] = None
168 return model
169
170 def _dir_model(self, name, path='', content=True):
171 """Build a model for a directory
172
173 if content is requested, will include a listing of the directory
174 """
175 os_path = self._get_os_path(name, path)
176
177 if not os.path.isdir(os_path):
178 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
179 elif is_hidden(os_path, self.root_dir):
180 self.log.info("Refusing to serve hidden directory, via 404 Error")
181 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
182
183 if name is None:
184 if '/' in path:
185 path, name = path.rsplit('/', 1)
186 else:
187 name = ''
188 model = self._base_model(name, path)
186 189 model['type'] = 'directory'
190 dir_path = u'{}/{}'.format(path, name)
191 if content:
192 contents = []
193 for os_path in glob.glob(self._get_os_path('*', dir_path)):
194 name = os.path.basename(os_path)
195 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
196 contents.append(self.get_model(name=name, path=dir_path, content=False))
197
198 model['content'] = sorted(contents, key=sort_key)
199
187 200 return model
188 201
189 def list_files(self, path):
190 """Returns a list of dictionaries that are the standard model
191 for all notebooks in the relative 'path'.
202 def _file_model(self, name, path='', content=True):
203 """Build a model for a file
192 204
193 Parameters
194 ----------
195 path : str
196 the URL path that describes the relative path for the
197 listed notebooks
205 if content is requested, include the file contents.
206 Text files will be unicode, binary files will be base64-encoded.
207 """
208 model = self._base_model(name, path)
209 model['type'] = 'file'
210 if content:
211 os_path = self._get_os_path(name, path)
212 try:
213 with io.open(os_path, 'r', encoding='utf-8') as f:
214 model['content'] = f.read()
215 except UnicodeError as e:
216 with io.open(os_path, 'rb') as f:
217 bcontent = f.read()
218 model['content'] = base64.encodestring(bcontent).decode('ascii')
219 model['format'] = 'base64'
220 else:
221 model['format'] = 'text'
222 return model
198 223
199 Returns
200 -------
201 notebooks : list of dicts
202 a list of the notebook models without 'content'
224
225 def _notebook_model(self, name, path='', content=True):
226 """Build a notebook model
227
228 if content is requested, the notebook content will be populated
229 as a JSON structure (not double-serialized)
203 230 """
204 path = path.strip('/')
205 names = self.get_names(path)
206 notebooks = [self.get(name, path, content=False)
207 for name in names if self.should_list(name)]
208 notebooks = sorted(notebooks, key=sort_key)
209 return notebooks
231 model = self._base_model(name, path)
232 model['type'] = 'notebook'
233 if content:
234 os_path = self._get_os_path(name, path)
235 with io.open(os_path, 'r', encoding='utf-8') as f:
236 try:
237 nb = current.read(f, u'json')
238 except Exception as e:
239 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
240 self.mark_trusted_cells(nb, name, path)
241 model['content'] = nb
242 model['format'] = 'json'
243 return model
210 244
211 def get(self, name, path='', content=True):
212 """ Takes a path and name for a notebook and returns its model
245 def get_model(self, name, path='', content=True):
246 """ Takes a path and name for an entity and returns its model
213 247
214 248 Parameters
215 249 ----------
216 250 name : str
217 the name of the notebook
251 the name of the target
218 252 path : str
219 253 the URL path that describes the relative path for
220 254 the notebook
221 255
222 256 Returns
223 257 -------
224 258 model : dict
225 the notebook model. If contents=True, returns the 'contents'
226 dict in the model as well.
259 the contents model. If content=True, returns the contents
260 of the file or directory as well.
227 261 """
228 262 path = path.strip('/')
229 if not self.file_exists(name=name, path=path):
230 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
263
264 if not self.exists(name=name, path=path):
265 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
266
231 267 os_path = self._get_os_path(name, path)
232 info = os.stat(os_path)
233 last_modified = tz.utcfromtimestamp(info.st_mtime)
234 created = tz.utcfromtimestamp(info.st_ctime)
235 # Create the notebook model.
236 model ={}
237 model['name'] = name
238 model['path'] = path
239 model['last_modified'] = last_modified
240 model['created'] = created
241 model['type'] = 'notebook'
242 if content:
243 with io.open(os_path, 'r', encoding='utf-8') as f:
244 try:
245 nb = current.read(f, u'json')
246 except Exception as e:
247 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
248 self.mark_trusted_cells(nb, name, path)
249 model['content'] = nb
268 if os.path.isdir(os_path):
269 model = self._dir_model(name, path, content)
270 elif name.endswith('.ipynb'):
271 model = self._notebook_model(name, path, content)
272 else:
273 model = self._file_model(name, path, content)
250 274 return model
251 275
252 276 def save(self, model, name='', path=''):
253 277 """Save the notebook model and return the model with no content."""
254 278 path = path.strip('/')
255 279
256 280 if 'content' not in model:
257 281 raise web.HTTPError(400, u'No notebook JSON data provided')
258 282
259 283 # One checkpoint should always exist
260 284 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
261 285 self.create_checkpoint(name, path)
262 286
263 287 new_path = model.get('path', path).strip('/')
264 288 new_name = model.get('name', name)
265 289
266 290 if path != new_path or name != new_name:
267 291 self.rename(name, path, new_name, new_path)
268 292
269 293 # Save the notebook file
270 294 os_path = self._get_os_path(new_name, new_path)
271 295 nb = current.to_notebook_json(model['content'])
272 296
273 297 self.check_and_sign(nb, new_name, new_path)
274 298
275 299 if 'name' in nb['metadata']:
276 300 nb['metadata']['name'] = u''
277 301 try:
278 302 self.log.debug("Autosaving notebook %s", os_path)
279 303 with io.open(os_path, 'w', encoding='utf-8') as f:
280 304 current.write(nb, f, u'json')
281 305 except Exception as e:
282 306 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
283 307
284 model = self.get(new_name, new_path, content=False)
308 model = self.get_model(new_name, new_path, content=False)
285 309 return model
286 310
287 311 def update(self, model, name, path=''):
288 312 """Update the file's path and/or name"""
289 313 path = path.strip('/')
290 314 new_name = model.get('name', name)
291 315 new_path = model.get('path', path).strip('/')
292 316 if path != new_path or name != new_name:
293 317 self.rename(name, path, new_name, new_path)
294 model = self.get(new_name, new_path, content=False)
318 model = self.get_model(new_name, new_path, content=False)
295 319 return model
296 320
297 321 def delete(self, name, path=''):
298 322 """Delete file by name and path."""
299 323 path = path.strip('/')
300 324 os_path = self._get_os_path(name, path)
301 325 if not os.path.isfile(os_path):
302 326 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
303 327
304 328 # clear checkpoints
305 329 for checkpoint in self.list_checkpoints(name, path):
306 330 checkpoint_id = checkpoint['id']
307 331 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
308 332 if os.path.isfile(cp_path):
309 333 self.log.debug("Unlinking checkpoint %s", cp_path)
310 334 os.unlink(cp_path)
311 335
312 336 self.log.debug("Unlinking file %s", os_path)
313 337 os.unlink(os_path)
314 338
315 339 def rename(self, old_name, old_path, new_name, new_path):
316 340 """Rename a file."""
317 341 old_path = old_path.strip('/')
318 342 new_path = new_path.strip('/')
319 343 if new_name == old_name and new_path == old_path:
320 344 return
321 345
322 346 new_os_path = self._get_os_path(new_name, new_path)
323 347 old_os_path = self._get_os_path(old_name, old_path)
324 348
325 349 # Should we proceed with the move?
326 350 if os.path.isfile(new_os_path):
327 351 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
328 352
329 353 # Move the file
330 354 try:
331 355 shutil.move(old_os_path, new_os_path)
332 356 except Exception as e:
333 357 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
334 358
335 359 # Move the checkpoints
336 360 old_checkpoints = self.list_checkpoints(old_name, old_path)
337 361 for cp in old_checkpoints:
338 362 checkpoint_id = cp['id']
339 363 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
340 364 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
341 365 if os.path.isfile(old_cp_path):
342 366 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
343 367 shutil.move(old_cp_path, new_cp_path)
344 368
345 369 # Checkpoint-related utilities
346 370
347 371 def get_checkpoint_path(self, checkpoint_id, name, path=''):
348 372 """find the path to a checkpoint"""
349 373 path = path.strip('/')
350 374 basename, ext = os.path.splitext(name)
351 375 filename = u"{name}-{checkpoint_id}{ext}".format(
352 376 name=basename,
353 377 checkpoint_id=checkpoint_id,
354 378 ext=ext,
355 379 )
356 380 os_path = self._get_os_path(path=path)
357 381 cp_dir = os.path.join(os_path, self.checkpoint_dir)
358 382 ensure_dir_exists(cp_dir)
359 383 cp_path = os.path.join(cp_dir, filename)
360 384 return cp_path
361 385
362 386 def get_checkpoint_model(self, checkpoint_id, name, path=''):
363 387 """construct the info dict for a given checkpoint"""
364 388 path = path.strip('/')
365 389 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
366 390 stats = os.stat(cp_path)
367 391 last_modified = tz.utcfromtimestamp(stats.st_mtime)
368 392 info = dict(
369 393 id = checkpoint_id,
370 394 last_modified = last_modified,
371 395 )
372 396 return info
373 397
374 398 # public checkpoint API
375 399
376 400 def create_checkpoint(self, name, path=''):
377 401 """Create a checkpoint from the current state of a file"""
378 402 path = path.strip('/')
379 403 src_path = self._get_os_path(name, path)
380 404 # only the one checkpoint ID:
381 405 checkpoint_id = u"checkpoint"
382 406 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
383 407 self.log.debug("creating checkpoint for notebook %s", name)
384 408 self._copy(src_path, cp_path)
385 409
386 410 # return the checkpoint info
387 411 return self.get_checkpoint_model(checkpoint_id, name, path)
388 412
389 413 def list_checkpoints(self, name, path=''):
390 414 """list the checkpoints for a given file
391 415
392 416 This contents manager currently only supports one checkpoint per file.
393 417 """
394 418 path = path.strip('/')
395 419 checkpoint_id = "checkpoint"
396 420 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
397 421 if not os.path.exists(os_path):
398 422 return []
399 423 else:
400 424 return [self.get_checkpoint_model(checkpoint_id, name, path)]
401 425
402 426
403 427 def restore_checkpoint(self, checkpoint_id, name, path=''):
404 428 """restore a file to a checkpointed state"""
405 429 path = path.strip('/')
406 430 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
407 431 nb_path = self._get_os_path(name, path)
408 432 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
409 433 if not os.path.isfile(cp_path):
410 434 self.log.debug("checkpoint file does not exist: %s", cp_path)
411 435 raise web.HTTPError(404,
412 436 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
413 437 )
414 438 # ensure notebook is readable (never restore from an unreadable notebook)
415 439 if cp_path.endswith('.ipynb'):
416 440 with io.open(cp_path, 'r', encoding='utf-8') as f:
417 441 current.read(f, u'json')
418 442 self._copy(cp_path, nb_path)
419 443 self.log.debug("copying %s -> %s", cp_path, nb_path)
420 444
421 445 def delete_checkpoint(self, checkpoint_id, name, path=''):
422 446 """delete a file's checkpoint"""
423 447 path = path.strip('/')
424 448 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
425 449 if not os.path.isfile(cp_path):
426 450 raise web.HTTPError(404,
427 451 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
428 452 )
429 453 self.log.debug("unlinking %s", cp_path)
430 454 os.unlink(cp_path)
431 455
432 456 def info_string(self):
433 457 return "Serving notebooks from local directory: %s" % self.root_dir
434 458
435 459 def get_kernel_path(self, name, path='', model=None):
436 460 """Return the initial working dir a kernel associated with a given notebook"""
437 461 return os.path.join(self.root_dir, path)
@@ -1,270 +1,273 b''
1 1 """Tornado handlers for the contents web service."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 from tornado import web
9 9
10 10 from IPython.html.utils import url_path_join, url_escape
11 11 from IPython.utils.jsonutil import date_default
12 12
13 13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
14 notebook_path_regex, path_regex,
15 notebook_name_regex)
14 file_path_regex, path_regex,
15 file_name_regex)
16 16
17 17
18 18 class ContentsHandler(IPythonHandler):
19 19
20 20 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
21 21
22 def location_url(self, name, path=''):
22 def location_url(self, name, path):
23 23 """Return the full URL location of a file.
24 24
25 25 Parameters
26 26 ----------
27 27 name : unicode
28 28 The base name of the file, such as "foo.ipynb".
29 29 path : unicode
30 30 The API path of the file, such as "foo/bar".
31 31 """
32 32 return url_escape(url_path_join(
33 33 self.base_url, 'api', 'contents', path, name
34 34 ))
35 35
36 36 def _finish_model(self, model, location=True):
37 37 """Finish a JSON request with a model, setting relevant headers, etc."""
38 38 if location:
39 39 location = self.location_url(model['name'], model['path'])
40 40 self.set_header('Location', location)
41 41 self.set_header('Last-Modified', model['last_modified'])
42 42 self.finish(json.dumps(model, default=date_default))
43 43
44 44 @web.authenticated
45 45 @json_errors
46 46 def get(self, path='', name=None):
47 47 """Return a file or list of files.
48 48
49 49 * GET with path and no filename lists files in a directory
50 50 * GET with path and filename returns file contents model
51 51 """
52 cm = self.contents_manager
53 # Check to see if a filename was given
54 if name is None:
55 # TODO: Remove this after we create the contents web service and directories are
56 # no longer listed by the notebook web service. This should only handle notebooks
57 # and not directories.
58 dirs = cm.list_dirs(path)
52 path = path or ''
53 model = self.contents_manager.get_model(name=name, path=path)
54 if model['type'] == 'directory':
55 # resort listing to group directories at the top
56 dirs = []
59 57 files = []
60 index = []
61 for nb in cm.list_files(path):
62 if nb['name'].lower() == 'index.ipynb':
63 index.append(nb)
58 for entry in model['content']:
59 if entry['type'] == 'directory':
60 dirs.append(entry)
64 61 else:
65 files.append(nb)
66 files = index + dirs + files
67 self.finish(json.dumps(files, default=date_default))
68 return
69 # get and return notebook representation
70 model = cm.get(name, path)
62 # do we also want to group notebooks separate from files?
63 files.append(entry)
64 model['content'] = dirs + files
71 65 self._finish_model(model, location=False)
72 66
73 67 @web.authenticated
74 68 @json_errors
75 69 def patch(self, path='', name=None):
76 70 """PATCH renames a notebook without re-uploading content."""
77 71 cm = self.contents_manager
78 72 if name is None:
79 73 raise web.HTTPError(400, u'Filename missing')
80 74 model = self.get_json_body()
81 75 if model is None:
82 76 raise web.HTTPError(400, u'JSON body missing')
83 77 model = cm.update(model, name, path)
84 78 self._finish_model(model)
85 79
86 80 def _copy(self, copy_from, path, copy_to=None):
87 81 """Copy a file in path, optionally specifying the new name.
88 82
89 83 Only support copying within the same directory.
90 84 """
91 85 self.log.info(u"Copying from %s/%s to %s/%s",
92 86 path, copy_from,
93 87 path, copy_to or '',
94 88 )
95 89 model = self.contents_manager.copy(copy_from, copy_to, path)
96 90 self.set_status(201)
97 91 self._finish_model(model)
98 92
99 93 def _upload(self, model, path, name=None):
100 94 """Upload a file
101 95
102 96 If name specified, create it in path/name.
103 97 """
104 98 self.log.info(u"Uploading file to %s/%s", path, name or '')
105 99 if name:
106 100 model['name'] = name
107 101
108 102 model = self.contents_manager.create_notebook(model, path)
109 103 self.set_status(201)
110 104 self._finish_model(model)
111 105
112 106 def _create_empty_notebook(self, path, name=None):
113 107 """Create an empty notebook in path
114 108
115 109 If name specified, create it in path/name.
116 110 """
117 111 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
118 112 model = {}
119 113 if name:
120 114 model['name'] = name
121 115 model = self.contents_manager.create_notebook(model, path=path)
122 116 self.set_status(201)
123 117 self._finish_model(model)
124 118
125 119 def _save(self, model, path, name):
126 120 """Save an existing file."""
127 121 self.log.info(u"Saving file at %s/%s", path, name)
128 122 model = self.contents_manager.save(model, name, path)
129 123 if model['path'] != path.strip('/') or model['name'] != name:
130 124 # a rename happened, set Location header
131 125 location = True
132 126 else:
133 127 location = False
134 128 self._finish_model(model, location)
135 129
136 130 @web.authenticated
137 131 @json_errors
138 132 def post(self, path='', name=None):
139 133 """Create a new notebook in the specified path.
140 134
141 135 POST creates new notebooks. The server always decides on the notebook name.
142 136
143 137 POST /api/contents/path
144 138 New untitled notebook in path. If content specified, upload a
145 139 notebook, otherwise start empty.
146 140 POST /api/contents/path?copy=OtherNotebook.ipynb
147 141 New copy of OtherNotebook in path
148 142 """
149 143
150 144 if name is not None:
145 path = u'{}/{}'.format(path, name)
146
147 cm = self.contents_manager
148
149 if cm.file_exists(path):
151 150 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
152 151
152 if not cm.path_exists(path):
153 raise web.HTTPError(404, "No such directory: %s" % path)
154
153 155 model = self.get_json_body()
154 156
155 157 if model is not None:
156 158 copy_from = model.get('copy_from')
157 159 if copy_from:
158 160 if model.get('content'):
159 161 raise web.HTTPError(400, "Can't upload and copy at the same time.")
160 162 self._copy(copy_from, path)
161 163 else:
162 164 self._upload(model, path)
163 165 else:
164 166 self._create_empty_notebook(path)
165 167
166 168 @web.authenticated
167 169 @json_errors
168 170 def put(self, path='', name=None):
169 171 """Saves the file in the location specified by name and path.
170 172
171 173 PUT is very similar to POST, but the requester specifies the name,
172 174 whereas with POST, the server picks the name.
173 175
174 176 PUT /api/contents/path/Name.ipynb
175 177 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
176 178 in `content` key of JSON request body. If content is not specified,
177 179 create a new empty notebook.
178 180 PUT /api/contents/path/Name.ipynb?copy=OtherNotebook.ipynb
179 181 Copy OtherNotebook to Name
180 182 """
181 183 if name is None:
182 184 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
183 185
184 186 model = self.get_json_body()
185 187 if model:
186 188 copy_from = model.get('copy_from')
187 189 if copy_from:
188 190 if model.get('content'):
189 191 raise web.HTTPError(400, "Can't upload and copy at the same time.")
190 192 self._copy(copy_from, path, name)
191 193 elif self.contents_manager.file_exists(name, path):
192 194 self._save(model, path, name)
193 195 else:
194 196 self._upload(model, path, name)
195 197 else:
196 198 self._create_empty_notebook(path, name)
197 199
198 200 @web.authenticated
199 201 @json_errors
200 202 def delete(self, path='', name=None):
201 203 """delete a file in the given path"""
202 204 cm = self.contents_manager
205 self.log.warn('delete %s:%s', path, name)
203 206 cm.delete(name, path)
204 207 self.set_status(204)
205 208 self.finish()
206 209
207 210
208 211 class CheckpointsHandler(IPythonHandler):
209 212
210 213 SUPPORTED_METHODS = ('GET', 'POST')
211 214
212 215 @web.authenticated
213 216 @json_errors
214 217 def get(self, path='', name=None):
215 218 """get lists checkpoints for a file"""
216 219 cm = self.contents_manager
217 220 checkpoints = cm.list_checkpoints(name, path)
218 221 data = json.dumps(checkpoints, default=date_default)
219 222 self.finish(data)
220 223
221 224 @web.authenticated
222 225 @json_errors
223 226 def post(self, path='', name=None):
224 227 """post creates a new checkpoint"""
225 228 cm = self.contents_manager
226 229 checkpoint = cm.create_checkpoint(name, path)
227 230 data = json.dumps(checkpoint, default=date_default)
228 231 location = url_path_join(self.base_url, 'api/contents',
229 232 path, name, 'checkpoints', checkpoint['id'])
230 233 self.set_header('Location', url_escape(location))
231 234 self.set_status(201)
232 235 self.finish(data)
233 236
234 237
235 238 class ModifyCheckpointsHandler(IPythonHandler):
236 239
237 240 SUPPORTED_METHODS = ('POST', 'DELETE')
238 241
239 242 @web.authenticated
240 243 @json_errors
241 244 def post(self, path, name, checkpoint_id):
242 245 """post restores a file from a checkpoint"""
243 246 cm = self.contents_manager
244 247 cm.restore_checkpoint(checkpoint_id, name, path)
245 248 self.set_status(204)
246 249 self.finish()
247 250
248 251 @web.authenticated
249 252 @json_errors
250 253 def delete(self, path, name, checkpoint_id):
251 254 """delete clears a checkpoint for a given file"""
252 255 cm = self.contents_manager
253 256 cm.delete_checkpoint(checkpoint_id, name, path)
254 257 self.set_status(204)
255 258 self.finish()
256 259
257 260 #-----------------------------------------------------------------------------
258 261 # URL to handler mappings
259 262 #-----------------------------------------------------------------------------
260 263
261 264
262 265 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
263 266
264 267 default_handlers = [
265 (r"/api/contents%s/checkpoints" % notebook_path_regex, CheckpointsHandler),
266 (r"/api/contents%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
268 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
269 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
267 270 ModifyCheckpointsHandler),
268 (r"/api/contents%s" % notebook_path_regex, ContentsHandler),
271 (r"/api/contents%s" % file_path_regex, ContentsHandler),
269 272 (r"/api/contents%s" % path_regex, ContentsHandler),
270 273 ]
@@ -1,267 +1,247 b''
1 1 """A base class for contents managers."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from fnmatch import fnmatch
7 7 import itertools
8 8 import os
9 9
10 10 from IPython.config.configurable import LoggingConfigurable
11 11 from IPython.nbformat import current, sign
12 12 from IPython.utils.traitlets import Instance, Unicode, List
13 13
14 14
15 15 class ContentsManager(LoggingConfigurable):
16 16
17 17 notary = Instance(sign.NotebookNotary)
18 18 def _notary_default(self):
19 19 return sign.NotebookNotary(parent=self)
20 20
21 21 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
22 22 Glob patterns to hide in file and directory listings.
23 23 """)
24 24
25 25 # ContentsManager API part 1: methods that must be
26 26 # implemented in subclasses.
27 27
28 28 def path_exists(self, path):
29 29 """Does the API-style path (directory) actually exist?
30 30
31 31 Override this method in subclasses.
32 32
33 33 Parameters
34 34 ----------
35 35 path : string
36 36 The path to check
37 37
38 38 Returns
39 39 -------
40 40 exists : bool
41 41 Whether the path does indeed exist.
42 42 """
43 43 raise NotImplementedError
44 44
45 45 def is_hidden(self, path):
46 46 """Does the API style path correspond to a hidden directory or file?
47 47
48 48 Parameters
49 49 ----------
50 50 path : string
51 51 The path to check. This is an API path (`/` separated,
52 52 relative to root dir).
53 53
54 54 Returns
55 55 -------
56 56 exists : bool
57 57 Whether the path is hidden.
58 58
59 59 """
60 60 raise NotImplementedError
61 61
62 62 def file_exists(self, name, path=''):
63 63 """Returns a True if the notebook exists. Else, returns False.
64 64
65 65 Parameters
66 66 ----------
67 67 name : string
68 68 The name of the notebook you are checking.
69 69 path : string
70 70 The relative path to the notebook (with '/' as separator)
71 71
72 72 Returns
73 73 -------
74 74 bool
75 75 """
76 76 raise NotImplementedError('must be implemented in a subclass')
77 77
78 # TODO: Remove this after we create the contents web service and directories are
79 # no longer listed by the notebook web service.
80 def list_dirs(self, path):
81 """List the directory models for a given API style path."""
82 raise NotImplementedError('must be implemented in a subclass')
83
84 # TODO: Remove this after we create the contents web service and directories are
85 # no longer listed by the notebook web service.
86 def get_dir_model(self, name, path=''):
87 """Get the directory model given a directory name and its API style path.
88
89 The keys in the model should be:
90 * name
91 * path
92 * last_modified
93 * created
94 * type='directory'
95 """
96 raise NotImplementedError('must be implemented in a subclass')
97
98 def list_files(self, path=''):
78 def list(self, path=''):
99 79 """Return a list of contents dicts without content.
100 80
101 81 This returns a list of dicts
102 82
103 83 This list of dicts should be sorted by name::
104 84
105 85 data = sorted(data, key=lambda item: item['name'])
106 86 """
107 87 raise NotImplementedError('must be implemented in a subclass')
108 88
109 89 def get_model(self, name, path='', content=True):
110 90 """Get the notebook model with or without content."""
111 91 raise NotImplementedError('must be implemented in a subclass')
112 92
113 93 def save(self, model, name, path=''):
114 94 """Save the notebook and return the model with no content."""
115 95 raise NotImplementedError('must be implemented in a subclass')
116 96
117 97 def update(self, model, name, path=''):
118 98 """Update the notebook and return the model with no content."""
119 99 raise NotImplementedError('must be implemented in a subclass')
120 100
121 101 def delete(self, name, path=''):
122 102 """Delete notebook by name and path."""
123 103 raise NotImplementedError('must be implemented in a subclass')
124 104
125 105 def create_checkpoint(self, name, path=''):
126 106 """Create a checkpoint of the current state of a notebook
127 107
128 108 Returns a checkpoint_id for the new checkpoint.
129 109 """
130 110 raise NotImplementedError("must be implemented in a subclass")
131 111
132 112 def list_checkpoints(self, name, path=''):
133 113 """Return a list of checkpoints for a given notebook"""
134 114 return []
135 115
136 116 def restore_checkpoint(self, checkpoint_id, name, path=''):
137 117 """Restore a notebook from one of its checkpoints"""
138 118 raise NotImplementedError("must be implemented in a subclass")
139 119
140 120 def delete_checkpoint(self, checkpoint_id, name, path=''):
141 121 """delete a checkpoint for a notebook"""
142 122 raise NotImplementedError("must be implemented in a subclass")
143 123
144 124 def info_string(self):
145 125 return "Serving notebooks"
146 126
147 127 # ContentsManager API part 2: methods that have useable default
148 128 # implementations, but can be overridden in subclasses.
149 129
150 130 def get_kernel_path(self, name, path='', model=None):
151 131 """ Return the path to start kernel in """
152 132 return path
153 133
154 134 def increment_filename(self, filename, path=''):
155 135 """Increment a filename until it is unique.
156 136
157 137 Parameters
158 138 ----------
159 139 filename : unicode
160 140 The name of a file, including extension
161 141 path : unicode
162 142 The URL path of the notebooks directory
163 143
164 144 Returns
165 145 -------
166 146 name : unicode
167 147 A filename that is unique, based on the input filename.
168 148 """
169 149 path = path.strip('/')
170 150 basename, ext = os.path.splitext(filename)
171 151 for i in itertools.count():
172 152 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
173 153 ext=ext)
174 154 if not self.file_exists(name, path):
175 155 break
176 156 return name
177 157
178 158 def create_notebook(self, model=None, path=''):
179 159 """Create a new notebook and return its model with no content."""
180 160 path = path.strip('/')
181 161 if model is None:
182 162 model = {}
183 163 if 'content' not in model:
184 164 metadata = current.new_metadata(name=u'')
185 165 model['content'] = current.new_notebook(metadata=metadata)
186 166 if 'name' not in model:
187 167 model['name'] = self.increment_filename('Untitled.ipynb', path)
188 168
189 169 model['path'] = path
190 170 model = self.save(model, model['name'], model['path'])
191 171 return model
192 172
193 173 def copy(self, from_name, to_name=None, path=''):
194 174 """Copy an existing file and return its new model.
195 175
196 176 If to_name not specified, increment `from_name-Copy#.ipynb`.
197 177 """
198 178 path = path.strip('/')
199 model = self.get(from_name, path)
179 model = self.get_model(from_name, path)
200 180 if not to_name:
201 181 base, ext = os.path.splitext(from_name)
202 182 copy_name = u'{0}-Copy{1}'.format(base, ext)
203 183 to_name = self.increment_filename(copy_name, path)
204 184 model['name'] = to_name
205 185 model = self.save(model, to_name, path)
206 186 return model
207 187
208 188 def log_info(self):
209 189 self.log.info(self.info_string())
210 190
211 191 def trust_notebook(self, name, path=''):
212 192 """Explicitly trust a notebook
213 193
214 194 Parameters
215 195 ----------
216 196 name : string
217 197 The filename of the notebook
218 198 path : string
219 199 The notebook's directory
220 200 """
221 model = self.get(name, path)
201 model = self.get_model(name, path)
222 202 nb = model['content']
223 203 self.log.warn("Trusting notebook %s/%s", path, name)
224 204 self.notary.mark_cells(nb, True)
225 205 self.save(model, name, path)
226 206
227 207 def check_and_sign(self, nb, name, path=''):
228 208 """Check for trusted cells, and sign the notebook.
229 209
230 210 Called as a part of saving notebooks.
231 211
232 212 Parameters
233 213 ----------
234 214 nb : dict
235 215 The notebook structure
236 216 name : string
237 217 The filename of the notebook
238 218 path : string
239 219 The notebook's directory
240 220 """
241 221 if self.notary.check_cells(nb):
242 222 self.notary.sign(nb)
243 223 else:
244 224 self.log.warn("Saving untrusted notebook %s/%s", path, name)
245 225
246 226 def mark_trusted_cells(self, nb, name, path=''):
247 227 """Mark cells as trusted if the notebook signature matches.
248 228
249 229 Called as a part of loading notebooks.
250 230
251 231 Parameters
252 232 ----------
253 233 nb : dict
254 234 The notebook structure
255 235 name : string
256 236 The filename of the notebook
257 237 path : string
258 238 The notebook's directory
259 239 """
260 240 trusted = self.notary.check_signature(nb)
261 241 if not trusted:
262 242 self.log.warn("Notebook %s/%s is not trusted", path, name)
263 243 self.notary.mark_cells(nb, trusted)
264 244
265 245 def should_list(self, name):
266 246 """Should this file/directory name be displayed in a listing?"""
267 247 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,346 +1,400 b''
1 1 # coding: utf-8
2 2 """Test the contents webservice API."""
3 3
4 import base64
4 5 import io
5 6 import json
6 7 import os
7 8 import shutil
8 9 from unicodedata import normalize
9 10
10 11 pjoin = os.path.join
11 12
12 13 import requests
13 14
14 15 from IPython.html.utils import url_path_join, url_escape
15 16 from IPython.html.tests.launchnotebook import NotebookTestBase, assert_http_error
16 17 from IPython.nbformat import current
17 18 from IPython.nbformat.current import (new_notebook, write, read, new_worksheet,
18 19 new_heading_cell, to_notebook_json)
19 20 from IPython.nbformat import v2
20 21 from IPython.utils import py3compat
21 22 from IPython.utils.data import uniq_stable
22 23
23 24
24 25 # TODO: Remove this after we create the contents web service and directories are
25 26 # no longer listed by the notebook web service.
26 def notebooks_only(nb_list):
27 return [nb for nb in nb_list if nb['type']=='notebook']
27 def notebooks_only(dir_model):
28 return [nb for nb in dir_model['content'] if nb['type']=='notebook']
28 29
29 def dirs_only(nb_list):
30 return [x for x in nb_list if x['type']=='directory']
30 def dirs_only(dir_model):
31 return [x for x in dir_model['content'] if x['type']=='directory']
31 32
32 33
33 34 class API(object):
34 35 """Wrapper for contents API calls."""
35 36 def __init__(self, base_url):
36 37 self.base_url = base_url
37 38
38 39 def _req(self, verb, path, body=None):
39 40 response = requests.request(verb,
40 41 url_path_join(self.base_url, 'api/contents', path),
41 42 data=body,
42 43 )
43 44 response.raise_for_status()
44 45 return response
45 46
46 47 def list(self, path='/'):
47 48 return self._req('GET', path)
48 49
49 50 def read(self, name, path='/'):
50 51 return self._req('GET', url_path_join(path, name))
51 52
52 53 def create_untitled(self, path='/'):
53 54 return self._req('POST', path)
54 55
55 56 def upload_untitled(self, body, path='/'):
56 57 return self._req('POST', path, body)
57 58
58 59 def copy_untitled(self, copy_from, path='/'):
59 60 body = json.dumps({'copy_from':copy_from})
60 61 return self._req('POST', path, body)
61 62
62 63 def create(self, name, path='/'):
63 64 return self._req('PUT', url_path_join(path, name))
64 65
65 66 def upload(self, name, body, path='/'):
66 67 return self._req('PUT', url_path_join(path, name), body)
67 68
68 69 def copy(self, copy_from, copy_to, path='/'):
69 70 body = json.dumps({'copy_from':copy_from})
70 71 return self._req('PUT', url_path_join(path, copy_to), body)
71 72
72 73 def save(self, name, body, path='/'):
73 74 return self._req('PUT', url_path_join(path, name), body)
74 75
75 76 def delete(self, name, path='/'):
76 77 return self._req('DELETE', url_path_join(path, name))
77 78
78 79 def rename(self, name, path, new_name):
79 80 body = json.dumps({'name': new_name})
80 81 return self._req('PATCH', url_path_join(path, name), body)
81 82
82 83 def get_checkpoints(self, name, path):
83 84 return self._req('GET', url_path_join(path, name, 'checkpoints'))
84 85
85 86 def new_checkpoint(self, name, path):
86 87 return self._req('POST', url_path_join(path, name, 'checkpoints'))
87 88
88 89 def restore_checkpoint(self, name, path, checkpoint_id):
89 90 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
90 91
91 92 def delete_checkpoint(self, name, path, checkpoint_id):
92 93 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
93 94
94 95 class APITest(NotebookTestBase):
95 96 """Test the kernels web service API"""
96 97 dirs_nbs = [('', 'inroot'),
97 98 ('Directory with spaces in', 'inspace'),
98 99 (u'unicodΓ©', 'innonascii'),
99 100 ('foo', 'a'),
100 101 ('foo', 'b'),
101 102 ('foo', 'name with spaces'),
102 103 ('foo', u'unicodΓ©'),
103 104 ('foo/bar', 'baz'),
104 105 ('ordering', 'A'),
105 106 ('ordering', 'b'),
106 107 ('ordering', 'C'),
107 108 (u'Γ₯ b', u'Γ§ d'),
108 109 ]
109 110 hidden_dirs = ['.hidden', '__pycache__']
110 111
111 112 dirs = uniq_stable([py3compat.cast_unicode(d) for (d,n) in dirs_nbs])
112 113 del dirs[0] # remove ''
113 114 top_level_dirs = {normalize('NFC', d.split('/')[0]) for d in dirs}
114 115
116 @staticmethod
117 def _blob_for_name(name):
118 return name.encode('utf-8') + b'\xFF'
119
120 @staticmethod
121 def _txt_for_name(name):
122 return u'%s text file' % name
123
115 124 def setUp(self):
116 125 nbdir = self.notebook_dir.name
126 self.blob = os.urandom(100)
127 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
128
129
117 130
118 131 for d in (self.dirs + self.hidden_dirs):
119 132 d.replace('/', os.sep)
120 133 if not os.path.isdir(pjoin(nbdir, d)):
121 134 os.mkdir(pjoin(nbdir, d))
122 135
123 136 for d, name in self.dirs_nbs:
124 137 d = d.replace('/', os.sep)
138 # create a notebook
125 139 with io.open(pjoin(nbdir, d, '%s.ipynb' % name), 'w',
126 140 encoding='utf-8') as f:
127 141 nb = new_notebook(name=name)
128 142 write(nb, f, format='ipynb')
129 143
144 # create a text file
145 with io.open(pjoin(nbdir, d, '%s.txt' % name), 'w',
146 encoding='utf-8') as f:
147 f.write(self._txt_for_name(name))
148
149 # create a binary file
150 with io.open(pjoin(nbdir, d, '%s.blob' % name), 'wb') as f:
151 f.write(self._blob_for_name(name))
152
130 153 self.api = API(self.base_url())
131 154
132 155 def tearDown(self):
133 156 nbdir = self.notebook_dir.name
134 157
135 158 for dname in (list(self.top_level_dirs) + self.hidden_dirs):
136 159 shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
137 160
138 161 if os.path.isfile(pjoin(nbdir, 'inroot.ipynb')):
139 162 os.unlink(pjoin(nbdir, 'inroot.ipynb'))
140 163
141 164 def test_list_notebooks(self):
142 165 nbs = notebooks_only(self.api.list().json())
143 166 self.assertEqual(len(nbs), 1)
144 167 self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
145 168
146 169 nbs = notebooks_only(self.api.list('/Directory with spaces in/').json())
147 170 self.assertEqual(len(nbs), 1)
148 171 self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
149 172
150 173 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
151 174 self.assertEqual(len(nbs), 1)
152 175 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
153 176 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
154 177
155 178 nbs = notebooks_only(self.api.list('/foo/bar/').json())
156 179 self.assertEqual(len(nbs), 1)
157 180 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
158 181 self.assertEqual(nbs[0]['path'], 'foo/bar')
159 182
160 183 nbs = notebooks_only(self.api.list('foo').json())
161 184 self.assertEqual(len(nbs), 4)
162 185 nbnames = { normalize('NFC', n['name']) for n in nbs }
163 186 expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodΓ©.ipynb']
164 187 expected = { normalize('NFC', name) for name in expected }
165 188 self.assertEqual(nbnames, expected)
166 189
167 190 nbs = notebooks_only(self.api.list('ordering').json())
168 191 nbnames = [n['name'] for n in nbs]
169 192 expected = ['A.ipynb', 'b.ipynb', 'C.ipynb']
170 193 self.assertEqual(nbnames, expected)
171 194
172 195 def test_list_dirs(self):
173 196 dirs = dirs_only(self.api.list().json())
174 197 dir_names = {normalize('NFC', d['name']) for d in dirs}
175 198 self.assertEqual(dir_names, self.top_level_dirs) # Excluding hidden dirs
176 199
177 200 def test_list_nonexistant_dir(self):
178 201 with assert_http_error(404):
179 202 self.api.list('nonexistant')
180 203
181 def test_get_contents(self):
204 def test_get_nb_contents(self):
182 205 for d, name in self.dirs_nbs:
183 206 nb = self.api.read('%s.ipynb' % name, d+'/').json()
184 207 self.assertEqual(nb['name'], u'%s.ipynb' % name)
208 self.assertEqual(nb['type'], 'notebook')
209 self.assertIn('content', nb)
210 self.assertEqual(nb['format'], 'json')
185 211 self.assertIn('content', nb)
186 212 self.assertIn('metadata', nb['content'])
187 213 self.assertIsInstance(nb['content']['metadata'], dict)
188 214
215 def test_get_contents_no_such_file(self):
189 216 # Name that doesn't exist - should be a 404
190 217 with assert_http_error(404):
191 218 self.api.read('q.ipynb', 'foo')
192 219
220 def test_get_text_file_contents(self):
221 for d, name in self.dirs_nbs:
222 model = self.api.read(u'%s.txt' % name, d+'/').json()
223 self.assertEqual(model['name'], u'%s.txt' % name)
224 self.assertIn('content', model)
225 self.assertEqual(model['format'], 'text')
226 self.assertEqual(model['type'], 'file')
227 self.assertEqual(model['content'], self._txt_for_name(name))
228
229 # Name that doesn't exist - should be a 404
230 with assert_http_error(404):
231 self.api.read('q.txt', 'foo')
232
233 def test_get_binary_file_contents(self):
234 for d, name in self.dirs_nbs:
235 model = self.api.read(u'%s.blob' % name, d+'/').json()
236 self.assertEqual(model['name'], u'%s.blob' % name)
237 self.assertIn('content', model)
238 self.assertEqual(model['format'], 'base64')
239 self.assertEqual(model['type'], 'file')
240 b64_data = base64.encodestring(self._blob_for_name(name)).decode('ascii')
241 self.assertEqual(model['content'], b64_data)
242
243 # Name that doesn't exist - should be a 404
244 with assert_http_error(404):
245 self.api.read('q.txt', 'foo')
246
193 247 def _check_nb_created(self, resp, name, path):
194 248 self.assertEqual(resp.status_code, 201)
195 249 location_header = py3compat.str_to_unicode(resp.headers['Location'])
196 250 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
197 251 self.assertEqual(resp.json()['name'], name)
198 252 assert os.path.isfile(pjoin(
199 253 self.notebook_dir.name,
200 254 path.replace('/', os.sep),
201 255 name,
202 256 ))
203 257
204 258 def test_create_untitled(self):
205 259 resp = self.api.create_untitled(path=u'Γ₯ b')
206 260 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
207 261
208 262 # Second time
209 263 resp = self.api.create_untitled(path=u'Γ₯ b')
210 264 self._check_nb_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
211 265
212 266 # And two directories down
213 267 resp = self.api.create_untitled(path='foo/bar')
214 268 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
215 269
216 270 def test_upload_untitled(self):
217 271 nb = new_notebook(name='Upload test')
218 272 nbmodel = {'content': nb}
219 273 resp = self.api.upload_untitled(path=u'Γ₯ b',
220 274 body=json.dumps(nbmodel))
221 275 self._check_nb_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
222 276
223 277 def test_upload(self):
224 278 nb = new_notebook(name=u'ignored')
225 279 nbmodel = {'content': nb}
226 280 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
227 281 body=json.dumps(nbmodel))
228 282 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
229 283
230 284 def test_upload_v2(self):
231 285 nb = v2.new_notebook()
232 286 ws = v2.new_worksheet()
233 287 nb.worksheets.append(ws)
234 288 ws.cells.append(v2.new_code_cell(input='print("hi")'))
235 289 nbmodel = {'content': nb}
236 290 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
237 291 body=json.dumps(nbmodel))
238 292 self._check_nb_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
239 293 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
240 294 data = resp.json()
241 295 self.assertEqual(data['content']['nbformat'], current.nbformat)
242 296 self.assertEqual(data['content']['orig_nbformat'], 2)
243 297
244 298 def test_copy_untitled(self):
245 299 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
246 300 self._check_nb_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
247 301
248 302 def test_copy(self):
249 303 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
250 304 self._check_nb_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
251 305
252 306 def test_delete(self):
253 307 for d, name in self.dirs_nbs:
254 308 resp = self.api.delete('%s.ipynb' % name, d)
255 309 self.assertEqual(resp.status_code, 204)
256 310
257 311 for d in self.dirs + ['/']:
258 312 nbs = notebooks_only(self.api.list(d).json())
259 313 self.assertEqual(len(nbs), 0)
260 314
261 315 def test_rename(self):
262 316 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
263 317 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
264 318 self.assertEqual(resp.json()['name'], 'z.ipynb')
265 319 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
266 320
267 321 nbs = notebooks_only(self.api.list('foo').json())
268 322 nbnames = set(n['name'] for n in nbs)
269 323 self.assertIn('z.ipynb', nbnames)
270 324 self.assertNotIn('a.ipynb', nbnames)
271 325
272 326 def test_rename_existing(self):
273 327 with assert_http_error(409):
274 328 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
275 329
276 330 def test_save(self):
277 331 resp = self.api.read('a.ipynb', 'foo')
278 332 nbcontent = json.loads(resp.text)['content']
279 333 nb = to_notebook_json(nbcontent)
280 334 ws = new_worksheet()
281 335 nb.worksheets = [ws]
282 336 ws.cells.append(new_heading_cell(u'Created by test Β³'))
283 337
284 338 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
285 339 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
286 340
287 341 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
288 342 with io.open(nbfile, 'r', encoding='utf-8') as f:
289 343 newnb = read(f, format='ipynb')
290 344 self.assertEqual(newnb.worksheets[0].cells[0].source,
291 345 u'Created by test Β³')
292 346 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
293 347 newnb = to_notebook_json(nbcontent)
294 348 self.assertEqual(newnb.worksheets[0].cells[0].source,
295 349 u'Created by test Β³')
296 350
297 351 # Save and rename
298 352 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
299 353 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
300 354 saved = resp.json()
301 355 self.assertEqual(saved['name'], 'a2.ipynb')
302 356 self.assertEqual(saved['path'], 'foo/bar')
303 357 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
304 358 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
305 359 with assert_http_error(404):
306 360 self.api.read('a.ipynb', 'foo')
307 361
308 362 def test_checkpoints(self):
309 363 resp = self.api.read('a.ipynb', 'foo')
310 364 r = self.api.new_checkpoint('a.ipynb', 'foo')
311 365 self.assertEqual(r.status_code, 201)
312 366 cp1 = r.json()
313 367 self.assertEqual(set(cp1), {'id', 'last_modified'})
314 368 self.assertEqual(r.headers['Location'].split('/')[-1], cp1['id'])
315 369
316 370 # Modify it
317 371 nbcontent = json.loads(resp.text)['content']
318 372 nb = to_notebook_json(nbcontent)
319 373 ws = new_worksheet()
320 374 nb.worksheets = [ws]
321 375 hcell = new_heading_cell('Created by test')
322 376 ws.cells.append(hcell)
323 377 # Save
324 378 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
325 379 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
326 380
327 381 # List checkpoints
328 382 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
329 383 self.assertEqual(cps, [cp1])
330 384
331 385 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
332 386 nb = to_notebook_json(nbcontent)
333 387 self.assertEqual(nb.worksheets[0].cells[0].source, 'Created by test')
334 388
335 389 # Restore cp1
336 390 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
337 391 self.assertEqual(r.status_code, 204)
338 392 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
339 393 nb = to_notebook_json(nbcontent)
340 394 self.assertEqual(nb.worksheets, [])
341 395
342 396 # Delete cp1
343 397 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
344 398 self.assertEqual(r.status_code, 204)
345 399 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
346 400 self.assertEqual(cps, [])
@@ -1,301 +1,301 b''
1 1 # coding: utf-8
2 2 """Tests for the notebook manager."""
3 3 from __future__ import print_function
4 4
5 5 import logging
6 6 import os
7 7
8 8 from tornado.web import HTTPError
9 9 from unittest import TestCase
10 10 from tempfile import NamedTemporaryFile
11 11
12 12 from IPython.nbformat import current
13 13
14 14 from IPython.utils.tempdir import TemporaryDirectory
15 15 from IPython.utils.traitlets import TraitError
16 16 from IPython.html.utils import url_path_join
17 17
18 18 from ..filemanager import FileContentsManager
19 19 from ..manager import ContentsManager
20 20
21 21
22 22 class TestFileContentsManager(TestCase):
23 23
24 24 def test_root_dir(self):
25 25 with TemporaryDirectory() as td:
26 26 fm = FileContentsManager(root_dir=td)
27 27 self.assertEqual(fm.root_dir, td)
28 28
29 29 def test_missing_root_dir(self):
30 30 with TemporaryDirectory() as td:
31 31 root = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
32 32 self.assertRaises(TraitError, FileContentsManager, root_dir=root)
33 33
34 34 def test_invalid_root_dir(self):
35 35 with NamedTemporaryFile() as tf:
36 36 self.assertRaises(TraitError, FileContentsManager, root_dir=tf.name)
37 37
38 38 def test_get_os_path(self):
39 39 # full filesystem path should be returned with correct operating system
40 40 # separators.
41 41 with TemporaryDirectory() as td:
42 42 root = td
43 43 fm = FileContentsManager(root_dir=root)
44 44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 46 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 47 self.assertEqual(path, fs_path)
48 48
49 49 fm = FileContentsManager(root_dir=root)
50 50 path = fm._get_os_path('test.ipynb')
51 51 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
52 52 self.assertEqual(path, fs_path)
53 53
54 54 fm = FileContentsManager(root_dir=root)
55 55 path = fm._get_os_path('test.ipynb', '////')
56 56 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 57 self.assertEqual(path, fs_path)
58 58
59 59 def test_checkpoint_subdir(self):
60 60 subd = u'sub βˆ‚ir'
61 61 cp_name = 'test-cp.ipynb'
62 62 with TemporaryDirectory() as td:
63 63 root = td
64 64 os.mkdir(os.path.join(td, subd))
65 65 fm = FileContentsManager(root_dir=root)
66 66 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 67 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 68 self.assertNotEqual(cp_dir, cp_subdir)
69 69 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 70 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71 71
72 72
73 class TestNotebookManager(TestCase):
73 class TestContentsManager(TestCase):
74 74
75 75 def setUp(self):
76 76 self._temp_dir = TemporaryDirectory()
77 77 self.td = self._temp_dir.name
78 78 self.contents_manager = FileContentsManager(
79 79 root_dir=self.td,
80 80 log=logging.getLogger()
81 81 )
82 82
83 83 def tearDown(self):
84 84 self._temp_dir.cleanup()
85 85
86 86 def make_dir(self, abs_path, rel_path):
87 87 """make subdirectory, rel_path is the relative path
88 88 to that directory from the location where the server started"""
89 89 os_path = os.path.join(abs_path, rel_path)
90 90 try:
91 91 os.makedirs(os_path)
92 92 except OSError:
93 93 print("Directory already exists: %r" % os_path)
94 94
95 95 def add_code_cell(self, nb):
96 96 output = current.new_output("display_data", output_javascript="alert('hi');")
97 97 cell = current.new_code_cell("print('hi')", outputs=[output])
98 98 if not nb.worksheets:
99 99 nb.worksheets.append(current.new_worksheet())
100 100 nb.worksheets[0].cells.append(cell)
101 101
102 102 def new_notebook(self):
103 103 cm = self.contents_manager
104 104 model = cm.create_notebook()
105 105 name = model['name']
106 106 path = model['path']
107 107
108 full_model = cm.get(name, path)
108 full_model = cm.get_model(name, path)
109 109 nb = full_model['content']
110 110 self.add_code_cell(nb)
111 111
112 112 cm.save(full_model, name, path)
113 113 return nb, name, path
114 114
115 115 def test_create_notebook(self):
116 116 cm = self.contents_manager
117 117 # Test in root directory
118 118 model = cm.create_notebook()
119 119 assert isinstance(model, dict)
120 120 self.assertIn('name', model)
121 121 self.assertIn('path', model)
122 122 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 123 self.assertEqual(model['path'], '')
124 124
125 125 # Test in sub-directory
126 126 sub_dir = '/foo/'
127 127 self.make_dir(cm.root_dir, 'foo')
128 128 model = cm.create_notebook(None, sub_dir)
129 129 assert isinstance(model, dict)
130 130 self.assertIn('name', model)
131 131 self.assertIn('path', model)
132 132 self.assertEqual(model['name'], 'Untitled0.ipynb')
133 133 self.assertEqual(model['path'], sub_dir.strip('/'))
134 134
135 135 def test_get(self):
136 136 cm = self.contents_manager
137 137 # Create a notebook
138 138 model = cm.create_notebook()
139 139 name = model['name']
140 140 path = model['path']
141 141
142 142 # Check that we 'get' on the notebook we just created
143 model2 = cm.get(name, path)
143 model2 = cm.get_model(name, path)
144 144 assert isinstance(model2, dict)
145 145 self.assertIn('name', model2)
146 146 self.assertIn('path', model2)
147 147 self.assertEqual(model['name'], name)
148 148 self.assertEqual(model['path'], path)
149 149
150 150 # Test in sub-directory
151 151 sub_dir = '/foo/'
152 152 self.make_dir(cm.root_dir, 'foo')
153 153 model = cm.create_notebook(None, sub_dir)
154 model2 = cm.get(name, sub_dir)
154 model2 = cm.get_model(name, sub_dir)
155 155 assert isinstance(model2, dict)
156 156 self.assertIn('name', model2)
157 157 self.assertIn('path', model2)
158 158 self.assertIn('content', model2)
159 159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 160 self.assertEqual(model2['path'], sub_dir.strip('/'))
161 161
162 162 def test_update(self):
163 163 cm = self.contents_manager
164 164 # Create a notebook
165 165 model = cm.create_notebook()
166 166 name = model['name']
167 167 path = model['path']
168 168
169 169 # Change the name in the model for rename
170 170 model['name'] = 'test.ipynb'
171 171 model = cm.update(model, name, path)
172 172 assert isinstance(model, dict)
173 173 self.assertIn('name', model)
174 174 self.assertIn('path', model)
175 175 self.assertEqual(model['name'], 'test.ipynb')
176 176
177 177 # Make sure the old name is gone
178 self.assertRaises(HTTPError, cm.get, name, path)
178 self.assertRaises(HTTPError, cm.get_model, name, path)
179 179
180 180 # Test in sub-directory
181 181 # Create a directory and notebook in that directory
182 182 sub_dir = '/foo/'
183 183 self.make_dir(cm.root_dir, 'foo')
184 184 model = cm.create_notebook(None, sub_dir)
185 185 name = model['name']
186 186 path = model['path']
187 187
188 188 # Change the name in the model for rename
189 189 model['name'] = 'test_in_sub.ipynb'
190 190 model = cm.update(model, name, path)
191 191 assert isinstance(model, dict)
192 192 self.assertIn('name', model)
193 193 self.assertIn('path', model)
194 194 self.assertEqual(model['name'], 'test_in_sub.ipynb')
195 195 self.assertEqual(model['path'], sub_dir.strip('/'))
196 196
197 197 # Make sure the old name is gone
198 self.assertRaises(HTTPError, cm.get, name, path)
198 self.assertRaises(HTTPError, cm.get_model, name, path)
199 199
200 200 def test_save(self):
201 201 cm = self.contents_manager
202 202 # Create a notebook
203 203 model = cm.create_notebook()
204 204 name = model['name']
205 205 path = model['path']
206 206
207 207 # Get the model with 'content'
208 full_model = cm.get(name, path)
208 full_model = cm.get_model(name, path)
209 209
210 210 # Save the notebook
211 211 model = cm.save(full_model, name, path)
212 212 assert isinstance(model, dict)
213 213 self.assertIn('name', model)
214 214 self.assertIn('path', model)
215 215 self.assertEqual(model['name'], name)
216 216 self.assertEqual(model['path'], path)
217 217
218 218 # Test in sub-directory
219 219 # Create a directory and notebook in that directory
220 220 sub_dir = '/foo/'
221 221 self.make_dir(cm.root_dir, 'foo')
222 222 model = cm.create_notebook(None, sub_dir)
223 223 name = model['name']
224 224 path = model['path']
225 model = cm.get(name, path)
225 model = cm.get_model(name, path)
226 226
227 227 # Change the name in the model for rename
228 228 model = cm.save(model, name, path)
229 229 assert isinstance(model, dict)
230 230 self.assertIn('name', model)
231 231 self.assertIn('path', model)
232 232 self.assertEqual(model['name'], 'Untitled0.ipynb')
233 233 self.assertEqual(model['path'], sub_dir.strip('/'))
234 234
235 235 def test_delete(self):
236 236 cm = self.contents_manager
237 237 # Create a notebook
238 238 nb, name, path = self.new_notebook()
239 239
240 240 # Delete the notebook
241 241 cm.delete(name, path)
242 242
243 243 # Check that a 'get' on the deleted notebook raises and error
244 self.assertRaises(HTTPError, cm.get, name, path)
244 self.assertRaises(HTTPError, cm.get_model, name, path)
245 245
246 246 def test_copy(self):
247 247 cm = self.contents_manager
248 248 path = u'Γ₯ b'
249 249 name = u'nb √.ipynb'
250 250 os.mkdir(os.path.join(cm.root_dir, path))
251 251 orig = cm.create_notebook({'name' : name}, path=path)
252 252
253 253 # copy with unspecified name
254 254 copy = cm.copy(name, path=path)
255 255 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
256 256
257 257 # copy with specified name
258 258 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
259 259 self.assertEqual(copy2['name'], u'copy 2.ipynb')
260 260
261 261 def test_trust_notebook(self):
262 262 cm = self.contents_manager
263 263 nb, name, path = self.new_notebook()
264 264
265 untrusted = cm.get(name, path)['content']
265 untrusted = cm.get_model(name, path)['content']
266 266 assert not cm.notary.check_cells(untrusted)
267 267
268 268 # print(untrusted)
269 269 cm.trust_notebook(name, path)
270 trusted = cm.get(name, path)['content']
270 trusted = cm.get_model(name, path)['content']
271 271 # print(trusted)
272 272 assert cm.notary.check_cells(trusted)
273 273
274 274 def test_mark_trusted_cells(self):
275 275 cm = self.contents_manager
276 276 nb, name, path = self.new_notebook()
277 277
278 278 cm.mark_trusted_cells(nb, name, path)
279 279 for cell in nb.worksheets[0].cells:
280 280 if cell.cell_type == 'code':
281 281 assert not cell.trusted
282 282
283 283 cm.trust_notebook(name, path)
284 nb = cm.get(name, path)['content']
284 nb = cm.get_model(name, path)['content']
285 285 for cell in nb.worksheets[0].cells:
286 286 if cell.cell_type == 'code':
287 287 assert cell.trusted
288 288
289 289 def test_check_and_sign(self):
290 290 cm = self.contents_manager
291 291 nb, name, path = self.new_notebook()
292 292
293 293 cm.mark_trusted_cells(nb, name, path)
294 294 cm.check_and_sign(nb, name, path)
295 295 assert not cm.notary.check_signature(nb)
296 296
297 297 cm.trust_notebook(name, path)
298 nb = cm.get(name, path)['content']
298 nb = cm.get_model(name, path)['content']
299 299 cm.mark_trusted_cells(nb, name, path)
300 300 cm.check_and_sign(nb, name, path)
301 301 assert cm.notary.check_signature(nb)
@@ -1,448 +1,449 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 ], function(IPython, $, utils, dialog) {
10 10 "use strict";
11 11
12 12 var NotebookList = function (selector, options) {
13 13 // Constructor
14 14 //
15 15 // Parameters:
16 16 // selector: string
17 17 // options: dictionary
18 18 // Dictionary of keyword arguments.
19 19 // session_list: SessionList instance
20 20 // element_name: string
21 21 // base_url: string
22 22 // notebook_path: string
23 23 var that = this;
24 24 this.session_list = options.session_list;
25 25 // allow code re-use by just changing element_name in kernellist.js
26 26 this.element_name = options.element_name || 'notebook';
27 27 this.selector = selector;
28 28 if (this.selector !== undefined) {
29 29 this.element = $(selector);
30 30 this.style();
31 31 this.bind_events();
32 32 }
33 33 this.notebooks_list = [];
34 34 this.sessions = {};
35 35 this.base_url = options.base_url || utils.get_body_data("baseUrl");
36 36 this.notebook_path = options.notebook_path || utils.get_body_data("notebookPath");
37 37 if (this.session_list && this.session_list.events) {
38 38 this.session_list.events.on('sessions_loaded.Dashboard',
39 39 function(e, d) { that.sessions_loaded(d); });
40 40 }
41 41 };
42 42
43 43 NotebookList.prototype.style = function () {
44 44 var prefix = '#' + this.element_name;
45 45 $(prefix + '_toolbar').addClass('list_toolbar');
46 46 $(prefix + '_list_info').addClass('toolbar_info');
47 47 $(prefix + '_buttons').addClass('toolbar_buttons');
48 48 $(prefix + '_list_header').addClass('list_header');
49 49 this.element.addClass("list_container");
50 50 };
51 51
52 52
53 53 NotebookList.prototype.bind_events = function () {
54 54 var that = this;
55 55 $('#refresh_' + this.element_name + '_list').click(function () {
56 56 that.load_sessions();
57 57 });
58 58 this.element.bind('dragover', function () {
59 59 return false;
60 60 });
61 61 this.element.bind('drop', function(event){
62 62 that.handleFilesUpload(event,'drop');
63 63 return false;
64 64 });
65 65 };
66 66
67 67 NotebookList.prototype.handleFilesUpload = function(event, dropOrForm) {
68 68 var that = this;
69 69 var files;
70 70 if(dropOrForm =='drop'){
71 71 files = event.originalEvent.dataTransfer.files;
72 72 } else
73 73 {
74 74 files = event.originalEvent.target.files;
75 75 }
76 76 for (var i = 0; i < files.length; i++) {
77 77 var f = files[i];
78 78 var reader = new FileReader();
79 79 reader.readAsText(f);
80 80 var name_and_ext = utils.splitext(f.name);
81 81 var file_ext = name_and_ext[1];
82 82 if (file_ext === '.ipynb') {
83 83 var item = that.new_notebook_item(0);
84 84 item.addClass('new-file');
85 85 that.add_name_input(f.name, item);
86 86 // Store the notebook item in the reader so we can use it later
87 87 // to know which item it belongs to.
88 88 $(reader).data('item', item);
89 89 reader.onload = function (event) {
90 90 var nbitem = $(event.target).data('item');
91 91 that.add_notebook_data(event.target.result, nbitem);
92 92 that.add_upload_button(nbitem);
93 93 };
94 94 } else {
95 95 var dialog_body = 'Uploaded notebooks must be .ipynb files';
96 96 dialog.modal({
97 97 title : 'Invalid file type',
98 98 body : dialog_body,
99 99 buttons : {'OK' : {'class' : 'btn-primary'}}
100 100 });
101 101 }
102 102 }
103 103 // Replace the file input form wth a clone of itself. This is required to
104 104 // reset the form. Otherwise, if you upload a file, delete it and try to
105 105 // upload it again, the changed event won't fire.
106 106 var form = $('input.fileinput');
107 107 form.replaceWith(form.clone(true));
108 108 return false;
109 109 };
110 110
111 111 NotebookList.prototype.clear_list = function (remove_uploads) {
112 112 // Clears the navigation tree.
113 113 //
114 114 // Parameters
115 115 // remove_uploads: bool=False
116 116 // Should upload prompts also be removed from the tree.
117 117 if (remove_uploads) {
118 118 this.element.children('.list_item').remove();
119 119 } else {
120 120 this.element.children('.list_item:not(.new-file)').remove();
121 121 }
122 122 };
123 123
124 124 NotebookList.prototype.load_sessions = function(){
125 125 this.session_list.load_sessions();
126 126 };
127 127
128 128
129 129 NotebookList.prototype.sessions_loaded = function(data){
130 130 this.sessions = data;
131 131 this.load_list();
132 132 };
133 133
134 134 NotebookList.prototype.load_list = function () {
135 135 var that = this;
136 136 var settings = {
137 137 processData : false,
138 138 cache : false,
139 139 type : "GET",
140 140 dataType : "json",
141 141 success : $.proxy(this.list_loaded, this),
142 142 error : $.proxy( function(xhr, status, error){
143 143 utils.log_ajax_error(xhr, status, error);
144 144 that.list_loaded([], null, null, {msg:"Error connecting to server."});
145 145 },this)
146 146 };
147 147
148 148 var url = utils.url_join_encode(
149 149 this.base_url,
150 150 'api',
151 151 'contents',
152 152 this.notebook_path
153 153 );
154 154 $.ajax(url, settings);
155 155 };
156 156
157 157
158 158 NotebookList.prototype.list_loaded = function (data, status, xhr, param) {
159 159 var message = 'Notebook list empty.';
160 160 if (param !== undefined && param.msg) {
161 161 message = param.msg;
162 162 }
163 163 var item = null;
164 var len = data.length;
164 var content = data.content;
165 var len = content.length;
165 166 this.clear_list();
166 167 if (len === 0) {
167 168 item = this.new_notebook_item(0);
168 169 var span12 = item.children().first();
169 170 span12.empty();
170 171 span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
171 172 }
172 173 var path = this.notebook_path;
173 174 var offset = 0;
174 175 if (path !== '') {
175 176 item = this.new_notebook_item(0);
176 177 this.add_dir(path, '..', item);
177 178 offset = 1;
178 179 }
179 180 for (var i=0; i<len; i++) {
180 if (data[i].type === 'directory') {
181 var name = data[i].name;
181 if (content[i].type === 'directory') {
182 var name = content[i].name;
182 183 item = this.new_notebook_item(i+offset);
183 184 this.add_dir(path, name, item);
184 185 } else {
185 var name = data[i].name;
186 var name = content[i].name;
186 187 item = this.new_notebook_item(i+offset);
187 188 this.add_link(path, name, item);
188 189 name = utils.url_path_join(path, name);
189 190 if(this.sessions[name] === undefined){
190 191 this.add_delete_button(item);
191 192 } else {
192 193 this.add_shutdown_button(item,this.sessions[name]);
193 194 }
194 195 }
195 196 }
196 197 };
197 198
198 199
199 200 NotebookList.prototype.new_notebook_item = function (index) {
200 201 var item = $('<div/>').addClass("list_item").addClass("row");
201 202 // item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
202 203 // item.css('border-top-style','none');
203 204 item.append($("<div/>").addClass("col-md-12").append(
204 205 $('<i/>').addClass('item_icon')
205 206 ).append(
206 207 $("<a/>").addClass("item_link").append(
207 208 $("<span/>").addClass("item_name")
208 209 )
209 210 ).append(
210 211 $('<div/>').addClass("item_buttons btn-group pull-right")
211 212 ));
212 213
213 214 if (index === -1) {
214 215 this.element.append(item);
215 216 } else {
216 217 this.element.children().eq(index).after(item);
217 218 }
218 219 return item;
219 220 };
220 221
221 222
222 223 NotebookList.prototype.add_dir = function (path, name, item) {
223 224 item.data('name', name);
224 225 item.data('path', path);
225 226 item.find(".item_name").text(name);
226 227 item.find(".item_icon").addClass('folder_icon').addClass('icon-fixed-width');
227 228 item.find("a.item_link")
228 229 .attr('href',
229 230 utils.url_join_encode(
230 231 this.base_url,
231 232 "tree",
232 233 path,
233 234 name
234 235 )
235 236 );
236 237 };
237 238
238 239
239 240 NotebookList.prototype.add_link = function (path, nbname, item) {
240 241 item.data('nbname', nbname);
241 242 item.data('path', path);
242 243 item.find(".item_name").text(nbname);
243 244 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
244 245 item.find("a.item_link")
245 246 .attr('href',
246 247 utils.url_join_encode(
247 248 this.base_url,
248 249 "notebooks",
249 250 path,
250 251 nbname
251 252 )
252 253 ).attr('target','_blank');
253 254 };
254 255
255 256
256 257 NotebookList.prototype.add_name_input = function (nbname, item) {
257 258 item.data('nbname', nbname);
258 259 item.find(".item_icon").addClass('notebook_icon').addClass('icon-fixed-width');
259 260 item.find(".item_name").empty().append(
260 261 $('<input/>')
261 262 .addClass("nbname_input")
262 263 .attr('value', utils.splitext(nbname)[0])
263 264 .attr('size', '30')
264 265 .attr('type', 'text')
265 266 );
266 267 };
267 268
268 269
269 270 NotebookList.prototype.add_notebook_data = function (data, item) {
270 271 item.data('nbdata', data);
271 272 };
272 273
273 274
274 275 NotebookList.prototype.add_shutdown_button = function (item, session) {
275 276 var that = this;
276 277 var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-xs btn-danger").
277 278 click(function (e) {
278 279 var settings = {
279 280 processData : false,
280 281 cache : false,
281 282 type : "DELETE",
282 283 dataType : "json",
283 284 success : function () {
284 285 that.load_sessions();
285 286 },
286 287 error : utils.log_ajax_error,
287 288 };
288 289 var url = utils.url_join_encode(
289 290 that.base_url,
290 291 'api/sessions',
291 292 session
292 293 );
293 294 $.ajax(url, settings);
294 295 return false;
295 296 });
296 297 // var new_buttons = item.find('a'); // shutdown_button;
297 298 item.find(".item_buttons").text("").append(shutdown_button);
298 299 };
299 300
300 301 NotebookList.prototype.add_delete_button = function (item) {
301 302 var new_buttons = $('<span/>').addClass("btn-group pull-right");
302 303 var notebooklist = this;
303 304 var delete_button = $("<button/>").text("Delete").addClass("btn btn-default btn-xs").
304 305 click(function (e) {
305 306 // $(this) is the button that was clicked.
306 307 var that = $(this);
307 308 // We use the nbname and notebook_id from the parent notebook_item element's
308 309 // data because the outer scopes values change as we iterate through the loop.
309 310 var parent_item = that.parents('div.list_item');
310 311 var nbname = parent_item.data('nbname');
311 312 var message = 'Are you sure you want to permanently delete the notebook: ' + nbname + '?';
312 313 dialog.modal({
313 314 title : "Delete notebook",
314 315 body : message,
315 316 buttons : {
316 317 Delete : {
317 318 class: "btn-danger",
318 319 click: function() {
319 320 var settings = {
320 321 processData : false,
321 322 cache : false,
322 323 type : "DELETE",
323 324 dataType : "json",
324 325 success : function (data, status, xhr) {
325 326 parent_item.remove();
326 327 },
327 328 error : utils.log_ajax_error,
328 329 };
329 330 var url = utils.url_join_encode(
330 331 notebooklist.base_url,
331 332 'api/contents',
332 333 notebooklist.notebook_path,
333 334 nbname
334 335 );
335 336 $.ajax(url, settings);
336 337 }
337 338 },
338 339 Cancel : {}
339 340 }
340 341 });
341 342 return false;
342 343 });
343 344 item.find(".item_buttons").text("").append(delete_button);
344 345 };
345 346
346 347
347 348 NotebookList.prototype.add_upload_button = function (item) {
348 349 var that = this;
349 350 var upload_button = $('<button/>').text("Upload")
350 351 .addClass('btn btn-primary btn-xs upload_button')
351 352 .click(function (e) {
352 353 var nbname = item.find('.item_name > input').val();
353 354 if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
354 355 nbname = nbname + ".ipynb";
355 356 }
356 357 var path = that.notebook_path;
357 358 var nbdata = item.data('nbdata');
358 359 var content_type = 'application/json';
359 360 var model = {
360 361 content : JSON.parse(nbdata),
361 362 };
362 363 var settings = {
363 364 processData : false,
364 365 cache : false,
365 366 type : 'PUT',
366 367 dataType : 'json',
367 368 data : JSON.stringify(model),
368 369 headers : {'Content-Type': content_type},
369 370 success : function (data, status, xhr) {
370 371 that.add_link(path, nbname, item);
371 372 that.add_delete_button(item);
372 373 },
373 374 error : utils.log_ajax_error,
374 375 };
375 376
376 377 var url = utils.url_join_encode(
377 378 that.base_url,
378 379 'api/contents',
379 380 that.notebook_path,
380 381 nbname
381 382 );
382 383 $.ajax(url, settings);
383 384 return false;
384 385 });
385 386 var cancel_button = $('<button/>').text("Cancel")
386 387 .addClass("btn btn-default btn-xs")
387 388 .click(function (e) {
388 389 console.log('cancel click');
389 390 item.remove();
390 391 return false;
391 392 });
392 393 item.find(".item_buttons").empty()
393 394 .append(upload_button)
394 395 .append(cancel_button);
395 396 };
396 397
397 398
398 399 NotebookList.prototype.new_notebook = function(){
399 400 var path = this.notebook_path;
400 401 var base_url = this.base_url;
401 402 var settings = {
402 403 processData : false,
403 404 cache : false,
404 405 type : "POST",
405 406 dataType : "json",
406 407 async : false,
407 408 success : function (data, status, xhr) {
408 409 var notebook_name = data.name;
409 410 window.open(
410 411 utils.url_join_encode(
411 412 base_url,
412 413 'notebooks',
413 414 path,
414 415 notebook_name),
415 416 '_blank'
416 417 );
417 418 },
418 419 error : $.proxy(this.new_notebook_failed, this),
419 420 };
420 421 var url = utils.url_join_encode(
421 422 base_url,
422 423 'api/contents',
423 424 path
424 425 );
425 426 $.ajax(url, settings);
426 427 };
427 428
428 429
429 430 NotebookList.prototype.new_notebook_failed = function (xhr, status, error) {
430 431 utils.log_ajax_error(xhr, status, error);
431 432 var msg;
432 433 if (xhr.responseJSON && xhr.responseJSON.message) {
433 434 msg = xhr.responseJSON.message;
434 435 } else {
435 436 msg = xhr.statusText;
436 437 }
437 438 dialog.modal({
438 439 title : 'Creating Notebook Failed',
439 440 body : "The error was: " + msg,
440 441 buttons : {'OK' : {'class' : 'btn-primary'}}
441 442 });
442 443 };
443 444
444 445 // Backwards compatability.
445 446 IPython.NotebookList = NotebookList;
446 447
447 448 return {'NotebookList': NotebookList};
448 449 });
@@ -1,101 +1,85 b''
1 """Tornado handlers for the tree view.
1 """Tornado handlers for the tree view."""
2 2
3 Authors:
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 5
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18 6 from tornado import web
19 7 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
20 8 from ..utils import url_path_join, url_escape
21 9
22 #-----------------------------------------------------------------------------
23 # Handlers
24 #-----------------------------------------------------------------------------
25
26 10
27 11 class TreeHandler(IPythonHandler):
28 12 """Render the tree view, listing notebooks, clusters, etc."""
29 13
30 14 def generate_breadcrumbs(self, path):
31 15 breadcrumbs = [(url_escape(url_path_join(self.base_url, 'tree')), '')]
32 16 comps = path.split('/')
33 17 ncomps = len(comps)
34 18 for i in range(ncomps):
35 19 if comps[i]:
36 20 link = url_escape(url_path_join(self.base_url, 'tree', *comps[0:i+1]))
37 21 breadcrumbs.append((link, comps[i]))
38 22 return breadcrumbs
39 23
40 24 def generate_page_title(self, path):
41 25 comps = path.split('/')
42 26 if len(comps) > 3:
43 27 for i in range(len(comps)-2):
44 28 comps.pop(0)
45 29 page_title = url_path_join(*comps)
46 30 if page_title:
47 31 return page_title+'/'
48 32 else:
49 33 return 'Home'
50 34
51 35 @web.authenticated
52 36 def get(self, path='', name=None):
53 37 path = path.strip('/')
54 38 cm = self.contents_manager
55 39 if name is not None:
56 40 # is a notebook, redirect to notebook handler
57 41 url = url_escape(url_path_join(
58 42 self.base_url, 'notebooks', path, name
59 43 ))
60 44 self.log.debug("Redirecting %s to %s", self.request.path, url)
61 45 self.redirect(url)
62 46 else:
63 47 if not cm.path_exists(path=path):
64 48 # Directory is hidden or does not exist.
65 49 raise web.HTTPError(404)
66 50 elif cm.is_hidden(path):
67 51 self.log.info("Refusing to serve hidden directory, via 404 Error")
68 52 raise web.HTTPError(404)
69 53 breadcrumbs = self.generate_breadcrumbs(path)
70 54 page_title = self.generate_page_title(path)
71 55 self.write(self.render_template('tree.html',
72 56 project=self.project_dir,
73 57 page_title=page_title,
74 58 notebook_path=path,
75 59 breadcrumbs=breadcrumbs
76 60 ))
77 61
78 62
79 63 class TreeRedirectHandler(IPythonHandler):
80 64 """Redirect a request to the corresponding tree URL"""
81 65
82 66 @web.authenticated
83 67 def get(self, path=''):
84 68 url = url_escape(url_path_join(
85 69 self.base_url, 'tree', path.strip('/')
86 70 ))
87 71 self.log.debug("Redirecting %s to %s", self.request.path, url)
88 72 self.redirect(url)
89 73
90 74
91 75 #-----------------------------------------------------------------------------
92 76 # URL to handler mappings
93 77 #-----------------------------------------------------------------------------
94 78
95 79
96 80 default_handlers = [
97 81 (r"/tree%s" % notebook_path_regex, TreeHandler),
98 82 (r"/tree%s" % path_regex, TreeHandler),
99 83 (r"/tree", TreeHandler),
100 84 (r"", TreeRedirectHandler),
101 85 ]
General Comments 0
You need to be logged in to leave comments. Login now