##// END OF EJS Templates
Small refactoring of is_hidden to take root as default kwarg.
Brian E. Granger -
Show More
@@ -1,387 +1,387 b''
1 """Base Tornado handlers for the notebook.
1 """Base Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19
19
20 import functools
20 import functools
21 import json
21 import json
22 import logging
22 import logging
23 import os
23 import os
24 import re
24 import re
25 import sys
25 import sys
26 import traceback
26 import traceback
27 try:
27 try:
28 # py3
28 # py3
29 from http.client import responses
29 from http.client import responses
30 except ImportError:
30 except ImportError:
31 from httplib import responses
31 from httplib import responses
32
32
33 from jinja2 import TemplateNotFound
33 from jinja2 import TemplateNotFound
34 from tornado import web
34 from tornado import web
35
35
36 try:
36 try:
37 from tornado.log import app_log
37 from tornado.log import app_log
38 except ImportError:
38 except ImportError:
39 app_log = logging.getLogger()
39 app_log = logging.getLogger()
40
40
41 from IPython.config import Application
41 from IPython.config import Application
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import string_types
43 from IPython.utils.py3compat import string_types
44 from IPython.html.utils import is_hidden
44 from IPython.html.utils import is_hidden
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Top-level handlers
47 # Top-level handlers
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
49 non_alphanum = re.compile(r'[^A-Za-z0-9]')
50
50
51 class AuthenticatedHandler(web.RequestHandler):
51 class AuthenticatedHandler(web.RequestHandler):
52 """A RequestHandler with an authenticated user."""
52 """A RequestHandler with an authenticated user."""
53
53
54 def clear_login_cookie(self):
54 def clear_login_cookie(self):
55 self.clear_cookie(self.cookie_name)
55 self.clear_cookie(self.cookie_name)
56
56
57 def get_current_user(self):
57 def get_current_user(self):
58 user_id = self.get_secure_cookie(self.cookie_name)
58 user_id = self.get_secure_cookie(self.cookie_name)
59 # For now the user_id should not return empty, but it could eventually
59 # For now the user_id should not return empty, but it could eventually
60 if user_id == '':
60 if user_id == '':
61 user_id = 'anonymous'
61 user_id = 'anonymous'
62 if user_id is None:
62 if user_id is None:
63 # prevent extra Invalid cookie sig warnings:
63 # prevent extra Invalid cookie sig warnings:
64 self.clear_login_cookie()
64 self.clear_login_cookie()
65 if not self.login_available:
65 if not self.login_available:
66 user_id = 'anonymous'
66 user_id = 'anonymous'
67 return user_id
67 return user_id
68
68
69 @property
69 @property
70 def cookie_name(self):
70 def cookie_name(self):
71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
71 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
72 self.request.host
72 self.request.host
73 ))
73 ))
74 return self.settings.get('cookie_name', default_cookie_name)
74 return self.settings.get('cookie_name', default_cookie_name)
75
75
76 @property
76 @property
77 def password(self):
77 def password(self):
78 """our password"""
78 """our password"""
79 return self.settings.get('password', '')
79 return self.settings.get('password', '')
80
80
81 @property
81 @property
82 def logged_in(self):
82 def logged_in(self):
83 """Is a user currently logged in?
83 """Is a user currently logged in?
84
84
85 """
85 """
86 user = self.get_current_user()
86 user = self.get_current_user()
87 return (user and not user == 'anonymous')
87 return (user and not user == 'anonymous')
88
88
89 @property
89 @property
90 def login_available(self):
90 def login_available(self):
91 """May a user proceed to log in?
91 """May a user proceed to log in?
92
92
93 This returns True if login capability is available, irrespective of
93 This returns True if login capability is available, irrespective of
94 whether the user is already logged in or not.
94 whether the user is already logged in or not.
95
95
96 """
96 """
97 return bool(self.settings.get('password', ''))
97 return bool(self.settings.get('password', ''))
98
98
99
99
100 class IPythonHandler(AuthenticatedHandler):
100 class IPythonHandler(AuthenticatedHandler):
101 """IPython-specific extensions to authenticated handling
101 """IPython-specific extensions to authenticated handling
102
102
103 Mostly property shortcuts to IPython-specific settings.
103 Mostly property shortcuts to IPython-specific settings.
104 """
104 """
105
105
106 @property
106 @property
107 def config(self):
107 def config(self):
108 return self.settings.get('config', None)
108 return self.settings.get('config', None)
109
109
110 @property
110 @property
111 def log(self):
111 def log(self):
112 """use the IPython log by default, falling back on tornado's logger"""
112 """use the IPython log by default, falling back on tornado's logger"""
113 if Application.initialized():
113 if Application.initialized():
114 return Application.instance().log
114 return Application.instance().log
115 else:
115 else:
116 return app_log
116 return app_log
117
117
118 #---------------------------------------------------------------
118 #---------------------------------------------------------------
119 # URLs
119 # URLs
120 #---------------------------------------------------------------
120 #---------------------------------------------------------------
121
121
122 @property
122 @property
123 def ws_url(self):
123 def ws_url(self):
124 """websocket url matching the current request
124 """websocket url matching the current request
125
125
126 By default, this is just `''`, indicating that it should match
126 By default, this is just `''`, indicating that it should match
127 the same host, protocol, port, etc.
127 the same host, protocol, port, etc.
128 """
128 """
129 return self.settings.get('websocket_url', '')
129 return self.settings.get('websocket_url', '')
130
130
131 @property
131 @property
132 def mathjax_url(self):
132 def mathjax_url(self):
133 return self.settings.get('mathjax_url', '')
133 return self.settings.get('mathjax_url', '')
134
134
135 @property
135 @property
136 def base_project_url(self):
136 def base_project_url(self):
137 return self.settings.get('base_project_url', '/')
137 return self.settings.get('base_project_url', '/')
138
138
139 @property
139 @property
140 def base_kernel_url(self):
140 def base_kernel_url(self):
141 return self.settings.get('base_kernel_url', '/')
141 return self.settings.get('base_kernel_url', '/')
142
142
143 #---------------------------------------------------------------
143 #---------------------------------------------------------------
144 # Manager objects
144 # Manager objects
145 #---------------------------------------------------------------
145 #---------------------------------------------------------------
146
146
147 @property
147 @property
148 def kernel_manager(self):
148 def kernel_manager(self):
149 return self.settings['kernel_manager']
149 return self.settings['kernel_manager']
150
150
151 @property
151 @property
152 def notebook_manager(self):
152 def notebook_manager(self):
153 return self.settings['notebook_manager']
153 return self.settings['notebook_manager']
154
154
155 @property
155 @property
156 def cluster_manager(self):
156 def cluster_manager(self):
157 return self.settings['cluster_manager']
157 return self.settings['cluster_manager']
158
158
159 @property
159 @property
160 def session_manager(self):
160 def session_manager(self):
161 return self.settings['session_manager']
161 return self.settings['session_manager']
162
162
163 @property
163 @property
164 def project_dir(self):
164 def project_dir(self):
165 return self.notebook_manager.notebook_dir
165 return self.notebook_manager.notebook_dir
166
166
167 #---------------------------------------------------------------
167 #---------------------------------------------------------------
168 # template rendering
168 # template rendering
169 #---------------------------------------------------------------
169 #---------------------------------------------------------------
170
170
171 def get_template(self, name):
171 def get_template(self, name):
172 """Return the jinja template object for a given name"""
172 """Return the jinja template object for a given name"""
173 return self.settings['jinja2_env'].get_template(name)
173 return self.settings['jinja2_env'].get_template(name)
174
174
175 def render_template(self, name, **ns):
175 def render_template(self, name, **ns):
176 ns.update(self.template_namespace)
176 ns.update(self.template_namespace)
177 template = self.get_template(name)
177 template = self.get_template(name)
178 return template.render(**ns)
178 return template.render(**ns)
179
179
180 @property
180 @property
181 def template_namespace(self):
181 def template_namespace(self):
182 return dict(
182 return dict(
183 base_project_url=self.base_project_url,
183 base_project_url=self.base_project_url,
184 base_kernel_url=self.base_kernel_url,
184 base_kernel_url=self.base_kernel_url,
185 logged_in=self.logged_in,
185 logged_in=self.logged_in,
186 login_available=self.login_available,
186 login_available=self.login_available,
187 static_url=self.static_url,
187 static_url=self.static_url,
188 )
188 )
189
189
190 def get_json_body(self):
190 def get_json_body(self):
191 """Return the body of the request as JSON data."""
191 """Return the body of the request as JSON data."""
192 if not self.request.body:
192 if not self.request.body:
193 return None
193 return None
194 # Do we need to call body.decode('utf-8') here?
194 # Do we need to call body.decode('utf-8') here?
195 body = self.request.body.strip().decode(u'utf-8')
195 body = self.request.body.strip().decode(u'utf-8')
196 try:
196 try:
197 model = json.loads(body)
197 model = json.loads(body)
198 except Exception:
198 except Exception:
199 self.log.debug("Bad JSON: %r", body)
199 self.log.debug("Bad JSON: %r", body)
200 self.log.error("Couldn't parse JSON", exc_info=True)
200 self.log.error("Couldn't parse JSON", exc_info=True)
201 raise web.HTTPError(400, u'Invalid JSON in body of request')
201 raise web.HTTPError(400, u'Invalid JSON in body of request')
202 return model
202 return model
203
203
204 def get_error_html(self, status_code, **kwargs):
204 def get_error_html(self, status_code, **kwargs):
205 """render custom error pages"""
205 """render custom error pages"""
206 exception = kwargs.get('exception')
206 exception = kwargs.get('exception')
207 message = ''
207 message = ''
208 status_message = responses.get(status_code, 'Unknown HTTP Error')
208 status_message = responses.get(status_code, 'Unknown HTTP Error')
209 if exception:
209 if exception:
210 # get the custom message, if defined
210 # get the custom message, if defined
211 try:
211 try:
212 message = exception.log_message % exception.args
212 message = exception.log_message % exception.args
213 except Exception:
213 except Exception:
214 pass
214 pass
215
215
216 # construct the custom reason, if defined
216 # construct the custom reason, if defined
217 reason = getattr(exception, 'reason', '')
217 reason = getattr(exception, 'reason', '')
218 if reason:
218 if reason:
219 status_message = reason
219 status_message = reason
220
220
221 # build template namespace
221 # build template namespace
222 ns = dict(
222 ns = dict(
223 status_code=status_code,
223 status_code=status_code,
224 status_message=status_message,
224 status_message=status_message,
225 message=message,
225 message=message,
226 exception=exception,
226 exception=exception,
227 )
227 )
228
228
229 # render the template
229 # render the template
230 try:
230 try:
231 html = self.render_template('%s.html' % status_code, **ns)
231 html = self.render_template('%s.html' % status_code, **ns)
232 except TemplateNotFound:
232 except TemplateNotFound:
233 self.log.debug("No template for %d", status_code)
233 self.log.debug("No template for %d", status_code)
234 html = self.render_template('error.html', **ns)
234 html = self.render_template('error.html', **ns)
235 return html
235 return html
236
236
237
237
238 class Template404(IPythonHandler):
238 class Template404(IPythonHandler):
239 """Render our 404 template"""
239 """Render our 404 template"""
240 def prepare(self):
240 def prepare(self):
241 raise web.HTTPError(404)
241 raise web.HTTPError(404)
242
242
243
243
244 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
244 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
245 """static files should only be accessible when logged in"""
245 """static files should only be accessible when logged in"""
246
246
247 @web.authenticated
247 @web.authenticated
248 def get(self, path):
248 def get(self, path):
249 if os.path.splitext(path)[1] == '.ipynb':
249 if os.path.splitext(path)[1] == '.ipynb':
250 name = os.path.basename(path)
250 name = os.path.basename(path)
251 self.set_header('Content-Type', 'application/json')
251 self.set_header('Content-Type', 'application/json')
252 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
252 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
253
253
254 return web.StaticFileHandler.get(self, path)
254 return web.StaticFileHandler.get(self, path)
255
255
256 def compute_etag(self):
256 def compute_etag(self):
257 return None
257 return None
258
258
259 def validate_absolute_path(self, root, absolute_path):
259 def validate_absolute_path(self, root, absolute_path):
260 """Validate and return the absolute path.
260 """Validate and return the absolute path.
261
261
262 Requires tornado 3.1
262 Requires tornado 3.1
263
263
264 Adding to tornado's own handling, forbids the serving of hidden files.
264 Adding to tornado's own handling, forbids the serving of hidden files.
265 """
265 """
266 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
266 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
267 abs_root = os.path.abspath(root)
267 abs_root = os.path.abspath(root)
268 if is_hidden(abs_root, abs_path):
268 if is_hidden(abs_path, abs_root):
269 raise web.HTTPError(404)
269 raise web.HTTPError(404)
270 return abs_path
270 return abs_path
271
271
272
272
273 def json_errors(method):
273 def json_errors(method):
274 """Decorate methods with this to return GitHub style JSON errors.
274 """Decorate methods with this to return GitHub style JSON errors.
275
275
276 This should be used on any JSON API on any handler method that can raise HTTPErrors.
276 This should be used on any JSON API on any handler method that can raise HTTPErrors.
277
277
278 This will grab the latest HTTPError exception using sys.exc_info
278 This will grab the latest HTTPError exception using sys.exc_info
279 and then:
279 and then:
280
280
281 1. Set the HTTP status code based on the HTTPError
281 1. Set the HTTP status code based on the HTTPError
282 2. Create and return a JSON body with a message field describing
282 2. Create and return a JSON body with a message field describing
283 the error in a human readable form.
283 the error in a human readable form.
284 """
284 """
285 @functools.wraps(method)
285 @functools.wraps(method)
286 def wrapper(self, *args, **kwargs):
286 def wrapper(self, *args, **kwargs):
287 try:
287 try:
288 result = method(self, *args, **kwargs)
288 result = method(self, *args, **kwargs)
289 except web.HTTPError as e:
289 except web.HTTPError as e:
290 status = e.status_code
290 status = e.status_code
291 message = e.log_message
291 message = e.log_message
292 self.set_status(e.status_code)
292 self.set_status(e.status_code)
293 self.finish(json.dumps(dict(message=message)))
293 self.finish(json.dumps(dict(message=message)))
294 except Exception:
294 except Exception:
295 self.log.error("Unhandled error in API request", exc_info=True)
295 self.log.error("Unhandled error in API request", exc_info=True)
296 status = 500
296 status = 500
297 message = "Unknown server error"
297 message = "Unknown server error"
298 t, value, tb = sys.exc_info()
298 t, value, tb = sys.exc_info()
299 self.set_status(status)
299 self.set_status(status)
300 tb_text = ''.join(traceback.format_exception(t, value, tb))
300 tb_text = ''.join(traceback.format_exception(t, value, tb))
301 reply = dict(message=message, traceback=tb_text)
301 reply = dict(message=message, traceback=tb_text)
302 self.finish(json.dumps(reply))
302 self.finish(json.dumps(reply))
303 else:
303 else:
304 return result
304 return result
305 return wrapper
305 return wrapper
306
306
307
307
308
308
309 #-----------------------------------------------------------------------------
309 #-----------------------------------------------------------------------------
310 # File handler
310 # File handler
311 #-----------------------------------------------------------------------------
311 #-----------------------------------------------------------------------------
312
312
313 # to minimize subclass changes:
313 # to minimize subclass changes:
314 HTTPError = web.HTTPError
314 HTTPError = web.HTTPError
315
315
316 class FileFindHandler(web.StaticFileHandler):
316 class FileFindHandler(web.StaticFileHandler):
317 """subclass of StaticFileHandler for serving files from a search path"""
317 """subclass of StaticFileHandler for serving files from a search path"""
318
318
319 # cache search results, don't search for files more than once
319 # cache search results, don't search for files more than once
320 _static_paths = {}
320 _static_paths = {}
321
321
322 def initialize(self, path, default_filename=None):
322 def initialize(self, path, default_filename=None):
323 if isinstance(path, string_types):
323 if isinstance(path, string_types):
324 path = [path]
324 path = [path]
325
325
326 self.root = tuple(
326 self.root = tuple(
327 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
327 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
328 )
328 )
329 self.default_filename = default_filename
329 self.default_filename = default_filename
330
330
331 def compute_etag(self):
331 def compute_etag(self):
332 return None
332 return None
333
333
334 @classmethod
334 @classmethod
335 def get_absolute_path(cls, roots, path):
335 def get_absolute_path(cls, roots, path):
336 """locate a file to serve on our static file search path"""
336 """locate a file to serve on our static file search path"""
337 with cls._lock:
337 with cls._lock:
338 if path in cls._static_paths:
338 if path in cls._static_paths:
339 return cls._static_paths[path]
339 return cls._static_paths[path]
340 try:
340 try:
341 abspath = os.path.abspath(filefind(path, roots))
341 abspath = os.path.abspath(filefind(path, roots))
342 except IOError:
342 except IOError:
343 # IOError means not found
343 # IOError means not found
344 return ''
344 return ''
345
345
346 cls._static_paths[path] = abspath
346 cls._static_paths[path] = abspath
347 return abspath
347 return abspath
348
348
349 def validate_absolute_path(self, root, absolute_path):
349 def validate_absolute_path(self, root, absolute_path):
350 """check if the file should be served (raises 404, 403, etc.)"""
350 """check if the file should be served (raises 404, 403, etc.)"""
351 if absolute_path == '':
351 if absolute_path == '':
352 raise web.HTTPError(404)
352 raise web.HTTPError(404)
353
353
354 for root in self.root:
354 for root in self.root:
355 if (absolute_path + os.sep).startswith(root):
355 if (absolute_path + os.sep).startswith(root):
356 break
356 break
357
357
358 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
358 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
359
359
360
360
361 class TrailingSlashHandler(web.RequestHandler):
361 class TrailingSlashHandler(web.RequestHandler):
362 """Simple redirect handler that strips trailing slashes
362 """Simple redirect handler that strips trailing slashes
363
363
364 This should be the first, highest priority handler.
364 This should be the first, highest priority handler.
365 """
365 """
366
366
367 SUPPORTED_METHODS = ['GET']
367 SUPPORTED_METHODS = ['GET']
368
368
369 def get(self):
369 def get(self):
370 self.redirect(self.request.uri.rstrip('/'))
370 self.redirect(self.request.uri.rstrip('/'))
371
371
372 #-----------------------------------------------------------------------------
372 #-----------------------------------------------------------------------------
373 # URL pattern fragments for re-use
373 # URL pattern fragments for re-use
374 #-----------------------------------------------------------------------------
374 #-----------------------------------------------------------------------------
375
375
376 path_regex = r"(?P<path>(?:/.*)*)"
376 path_regex = r"(?P<path>(?:/.*)*)"
377 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
377 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
378 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
378 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
379
379
380 #-----------------------------------------------------------------------------
380 #-----------------------------------------------------------------------------
381 # URL to handler mappings
381 # URL to handler mappings
382 #-----------------------------------------------------------------------------
382 #-----------------------------------------------------------------------------
383
383
384
384
385 default_handlers = [
385 default_handlers = [
386 (r".*/", TrailingSlashHandler)
386 (r".*/", TrailingSlashHandler)
387 ]
387 ]
@@ -1,477 +1,477 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import io
20 import io
21 import itertools
21 import itertools
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from tornado import web
26 from tornado import web
27
27
28 from .nbmanager import NotebookManager
28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
29 from IPython.nbformat import current
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
31 from IPython.utils import tz
32 from IPython.html.utils import is_hidden
32 from IPython.html.utils import is_hidden
33
33
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class FileNotebookManager(NotebookManager):
38 class FileNotebookManager(NotebookManager):
39
39
40 save_script = Bool(False, config=True,
40 save_script = Bool(False, config=True,
41 help="""Automatically create a Python script when saving the notebook.
41 help="""Automatically create a Python script when saving the notebook.
42
42
43 For easier use of import, %run and %load across notebooks, a
43 For easier use of import, %run and %load across notebooks, a
44 <notebook-name>.py script will be created next to any
44 <notebook-name>.py script will be created next to any
45 <notebook-name>.ipynb on each save. This can also be set with the
45 <notebook-name>.ipynb on each save. This can also be set with the
46 short `--script` flag.
46 short `--script` flag.
47 """
47 """
48 )
48 )
49
49
50 checkpoint_dir = Unicode(config=True,
50 checkpoint_dir = Unicode(config=True,
51 help="""The location in which to keep notebook checkpoints
51 help="""The location in which to keep notebook checkpoints
52
52
53 By default, it is notebook-dir/.ipynb_checkpoints
53 By default, it is notebook-dir/.ipynb_checkpoints
54 """
54 """
55 )
55 )
56 def _checkpoint_dir_default(self):
56 def _checkpoint_dir_default(self):
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
58
58
59 def _checkpoint_dir_changed(self, name, old, new):
59 def _checkpoint_dir_changed(self, name, old, new):
60 """do a bit of validation of the checkpoint dir"""
60 """do a bit of validation of the checkpoint dir"""
61 if not os.path.isabs(new):
61 if not os.path.isabs(new):
62 # If we receive a non-absolute path, make it absolute.
62 # If we receive a non-absolute path, make it absolute.
63 abs_new = os.path.abspath(new)
63 abs_new = os.path.abspath(new)
64 self.checkpoint_dir = abs_new
64 self.checkpoint_dir = abs_new
65 return
65 return
66 if os.path.exists(new) and not os.path.isdir(new):
66 if os.path.exists(new) and not os.path.isdir(new):
67 raise TraitError("checkpoint dir %r is not a directory" % new)
67 raise TraitError("checkpoint dir %r is not a directory" % new)
68 if not os.path.exists(new):
68 if not os.path.exists(new):
69 self.log.info("Creating checkpoint dir %s", new)
69 self.log.info("Creating checkpoint dir %s", new)
70 try:
70 try:
71 os.mkdir(new)
71 os.mkdir(new)
72 except:
72 except:
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
73 raise TraitError("Couldn't create checkpoint dir %r" % new)
74
74
75 def get_notebook_names(self, path=''):
75 def get_notebook_names(self, path=''):
76 """List all notebook names in the notebook dir and path."""
76 """List all notebook names in the notebook dir and path."""
77 path = path.strip('/')
77 path = path.strip('/')
78 if not os.path.isdir(self.get_os_path(path=path)):
78 if not os.path.isdir(self.get_os_path(path=path)):
79 raise web.HTTPError(404, 'Directory not found: ' + path)
79 raise web.HTTPError(404, 'Directory not found: ' + path)
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
81 names = [os.path.basename(name)
81 names = [os.path.basename(name)
82 for name in names]
82 for name in names]
83 return names
83 return names
84
84
85 def increment_filename(self, basename, path='', ext='.ipynb'):
85 def increment_filename(self, basename, path='', ext='.ipynb'):
86 """Return a non-used filename of the form basename<int>."""
86 """Return a non-used filename of the form basename<int>."""
87 path = path.strip('/')
87 path = path.strip('/')
88 for i in itertools.count():
88 for i in itertools.count():
89 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
90 os_path = self.get_os_path(name, path)
90 os_path = self.get_os_path(name, path)
91 if not os.path.isfile(os_path):
91 if not os.path.isfile(os_path):
92 break
92 break
93 return name
93 return name
94
94
95 def path_exists(self, path):
95 def path_exists(self, path):
96 """Does the API-style path (directory) actually exist?
96 """Does the API-style path (directory) actually exist?
97
97
98 Parameters
98 Parameters
99 ----------
99 ----------
100 path : string
100 path : string
101 The path to check. This is an API path (`/` separated,
101 The path to check. This is an API path (`/` separated,
102 relative to base notebook-dir).
102 relative to base notebook-dir).
103
103
104 Returns
104 Returns
105 -------
105 -------
106 exists : bool
106 exists : bool
107 Whether the path is indeed a directory.
107 Whether the path is indeed a directory.
108 """
108 """
109 path = path.strip('/')
109 path = path.strip('/')
110 os_path = self.get_os_path(path=path)
110 os_path = self.get_os_path(path=path)
111 return os.path.isdir(os_path)
111 return os.path.isdir(os_path)
112
112
113 def is_hidden(self, path):
113 def is_hidden(self, path):
114 """Does the API style path correspond to a hidden directory or file?
114 """Does the API style path correspond to a hidden directory or file?
115
115
116 Parameters
116 Parameters
117 ----------
117 ----------
118 path : string
118 path : string
119 The path to check. This is an API path (`/` separated,
119 The path to check. This is an API path (`/` separated,
120 relative to base notebook-dir).
120 relative to base notebook-dir).
121
121
122 Returns
122 Returns
123 -------
123 -------
124 exists : bool
124 exists : bool
125 Whether the path is hidden.
125 Whether the path is hidden.
126
126
127 """
127 """
128 path = path.strip('/')
128 path = path.strip('/')
129 os_path = self.get_os_path(path=path)
129 os_path = self.get_os_path(path=path)
130 return is_hidden(self.notebook_dir, os_path)
130 return is_hidden(os_path, self.notebook_dir)
131
131
132 def get_os_path(self, name=None, path=''):
132 def get_os_path(self, name=None, path=''):
133 """Given a notebook name and a URL path, return its file system
133 """Given a notebook name and a URL path, return its file system
134 path.
134 path.
135
135
136 Parameters
136 Parameters
137 ----------
137 ----------
138 name : string
138 name : string
139 The name of a notebook file with the .ipynb extension
139 The name of a notebook file with the .ipynb extension
140 path : string
140 path : string
141 The relative URL path (with '/' as separator) to the named
141 The relative URL path (with '/' as separator) to the named
142 notebook.
142 notebook.
143
143
144 Returns
144 Returns
145 -------
145 -------
146 path : string
146 path : string
147 A file system path that combines notebook_dir (location where
147 A file system path that combines notebook_dir (location where
148 server started), the relative path, and the filename with the
148 server started), the relative path, and the filename with the
149 current operating system's url.
149 current operating system's url.
150 """
150 """
151 parts = path.strip('/').split('/')
151 parts = path.strip('/').split('/')
152 parts = [p for p in parts if p != ''] # remove duplicate splits
152 parts = [p for p in parts if p != ''] # remove duplicate splits
153 if name is not None:
153 if name is not None:
154 parts.append(name)
154 parts.append(name)
155 path = os.path.join(self.notebook_dir, *parts)
155 path = os.path.join(self.notebook_dir, *parts)
156 return path
156 return path
157
157
158 def notebook_exists(self, name, path=''):
158 def notebook_exists(self, name, path=''):
159 """Returns a True if the notebook exists. Else, returns False.
159 """Returns a True if the notebook exists. Else, returns False.
160
160
161 Parameters
161 Parameters
162 ----------
162 ----------
163 name : string
163 name : string
164 The name of the notebook you are checking.
164 The name of the notebook you are checking.
165 path : string
165 path : string
166 The relative path to the notebook (with '/' as separator)
166 The relative path to the notebook (with '/' as separator)
167
167
168 Returns
168 Returns
169 -------
169 -------
170 bool
170 bool
171 """
171 """
172 path = path.strip('/')
172 path = path.strip('/')
173 nbpath = self.get_os_path(name, path=path)
173 nbpath = self.get_os_path(name, path=path)
174 return os.path.isfile(nbpath)
174 return os.path.isfile(nbpath)
175
175
176 # TODO: Remove this after we create the contents web service and directories are
176 # TODO: Remove this after we create the contents web service and directories are
177 # no longer listed by the notebook web service.
177 # no longer listed by the notebook web service.
178 def list_dirs(self, path):
178 def list_dirs(self, path):
179 """List the directories for a given API style path."""
179 """List the directories for a given API style path."""
180 path = path.strip('/')
180 path = path.strip('/')
181 os_path = self.get_os_path('', path)
181 os_path = self.get_os_path('', path)
182 if not os.path.isdir(os_path) or is_hidden(self.notebook_dir, os_path):
182 if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
183 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
183 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
184 dir_names = os.listdir(os_path)
184 dir_names = os.listdir(os_path)
185 dirs = []
185 dirs = []
186 for name in dir_names:
186 for name in dir_names:
187 os_path = self.get_os_path(name, path)
187 os_path = self.get_os_path(name, path)
188 if os.path.isdir(os_path) and not is_hidden(self.notebook_dir, os_path):
188 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
189 try:
189 try:
190 model = self.get_dir_model(name, path)
190 model = self.get_dir_model(name, path)
191 except IOError:
191 except IOError:
192 pass
192 pass
193 dirs.append(model)
193 dirs.append(model)
194 dirs = sorted(dirs, key=lambda item: item['name'])
194 dirs = sorted(dirs, key=lambda item: item['name'])
195 return dirs
195 return dirs
196
196
197 # TODO: Remove this after we create the contents web service and directories are
197 # TODO: Remove this after we create the contents web service and directories are
198 # no longer listed by the notebook web service.
198 # no longer listed by the notebook web service.
199 def get_dir_model(self, name, path=''):
199 def get_dir_model(self, name, path=''):
200 """Get the directory model given a directory name and its API style path"""
200 """Get the directory model given a directory name and its API style path"""
201 path = path.strip('/')
201 path = path.strip('/')
202 os_path = self.get_os_path(name, path)
202 os_path = self.get_os_path(name, path)
203 if not os.path.isdir(os_path):
203 if not os.path.isdir(os_path):
204 raise IOError('directory does not exist: %r' % os_path)
204 raise IOError('directory does not exist: %r' % os_path)
205 info = os.stat(os_path)
205 info = os.stat(os_path)
206 last_modified = tz.utcfromtimestamp(info.st_mtime)
206 last_modified = tz.utcfromtimestamp(info.st_mtime)
207 created = tz.utcfromtimestamp(info.st_ctime)
207 created = tz.utcfromtimestamp(info.st_ctime)
208 # Create the notebook model.
208 # Create the notebook model.
209 model ={}
209 model ={}
210 model['name'] = name
210 model['name'] = name
211 model['path'] = path
211 model['path'] = path
212 model['last_modified'] = last_modified
212 model['last_modified'] = last_modified
213 model['created'] = created
213 model['created'] = created
214 model['type'] = 'directory'
214 model['type'] = 'directory'
215 return model
215 return model
216
216
217 def list_notebooks(self, path):
217 def list_notebooks(self, path):
218 """Returns a list of dictionaries that are the standard model
218 """Returns a list of dictionaries that are the standard model
219 for all notebooks in the relative 'path'.
219 for all notebooks in the relative 'path'.
220
220
221 Parameters
221 Parameters
222 ----------
222 ----------
223 path : str
223 path : str
224 the URL path that describes the relative path for the
224 the URL path that describes the relative path for the
225 listed notebooks
225 listed notebooks
226
226
227 Returns
227 Returns
228 -------
228 -------
229 notebooks : list of dicts
229 notebooks : list of dicts
230 a list of the notebook models without 'content'
230 a list of the notebook models without 'content'
231 """
231 """
232 path = path.strip('/')
232 path = path.strip('/')
233 notebook_names = self.get_notebook_names(path)
233 notebook_names = self.get_notebook_names(path)
234 notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names]
234 notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names]
235 notebooks = sorted(notebooks, key=lambda item: item['name'])
235 notebooks = sorted(notebooks, key=lambda item: item['name'])
236 return notebooks
236 return notebooks
237
237
238 def get_notebook_model(self, name, path='', content=True):
238 def get_notebook_model(self, name, path='', content=True):
239 """ Takes a path and name for a notebook and returns its model
239 """ Takes a path and name for a notebook and returns its model
240
240
241 Parameters
241 Parameters
242 ----------
242 ----------
243 name : str
243 name : str
244 the name of the notebook
244 the name of the notebook
245 path : str
245 path : str
246 the URL path that describes the relative path for
246 the URL path that describes the relative path for
247 the notebook
247 the notebook
248
248
249 Returns
249 Returns
250 -------
250 -------
251 model : dict
251 model : dict
252 the notebook model. If contents=True, returns the 'contents'
252 the notebook model. If contents=True, returns the 'contents'
253 dict in the model as well.
253 dict in the model as well.
254 """
254 """
255 path = path.strip('/')
255 path = path.strip('/')
256 if not self.notebook_exists(name=name, path=path):
256 if not self.notebook_exists(name=name, path=path):
257 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
257 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
258 os_path = self.get_os_path(name, path)
258 os_path = self.get_os_path(name, path)
259 info = os.stat(os_path)
259 info = os.stat(os_path)
260 last_modified = tz.utcfromtimestamp(info.st_mtime)
260 last_modified = tz.utcfromtimestamp(info.st_mtime)
261 created = tz.utcfromtimestamp(info.st_ctime)
261 created = tz.utcfromtimestamp(info.st_ctime)
262 # Create the notebook model.
262 # Create the notebook model.
263 model ={}
263 model ={}
264 model['name'] = name
264 model['name'] = name
265 model['path'] = path
265 model['path'] = path
266 model['last_modified'] = last_modified
266 model['last_modified'] = last_modified
267 model['created'] = created
267 model['created'] = created
268 model['type'] = 'notebook'
268 model['type'] = 'notebook'
269 if content:
269 if content:
270 with io.open(os_path, 'r', encoding='utf-8') as f:
270 with io.open(os_path, 'r', encoding='utf-8') as f:
271 try:
271 try:
272 nb = current.read(f, u'json')
272 nb = current.read(f, u'json')
273 except Exception as e:
273 except Exception as e:
274 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
274 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
275 self.mark_trusted_cells(nb, path, name)
275 self.mark_trusted_cells(nb, path, name)
276 model['content'] = nb
276 model['content'] = nb
277 return model
277 return model
278
278
279 def save_notebook_model(self, model, name='', path=''):
279 def save_notebook_model(self, model, name='', path=''):
280 """Save the notebook model and return the model with no content."""
280 """Save the notebook model and return the model with no content."""
281 path = path.strip('/')
281 path = path.strip('/')
282
282
283 if 'content' not in model:
283 if 'content' not in model:
284 raise web.HTTPError(400, u'No notebook JSON data provided')
284 raise web.HTTPError(400, u'No notebook JSON data provided')
285
285
286 # One checkpoint should always exist
286 # One checkpoint should always exist
287 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
287 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
288 self.create_checkpoint(name, path)
288 self.create_checkpoint(name, path)
289
289
290 new_path = model.get('path', path).strip('/')
290 new_path = model.get('path', path).strip('/')
291 new_name = model.get('name', name)
291 new_name = model.get('name', name)
292
292
293 if path != new_path or name != new_name:
293 if path != new_path or name != new_name:
294 self.rename_notebook(name, path, new_name, new_path)
294 self.rename_notebook(name, path, new_name, new_path)
295
295
296 # Save the notebook file
296 # Save the notebook file
297 os_path = self.get_os_path(new_name, new_path)
297 os_path = self.get_os_path(new_name, new_path)
298 nb = current.to_notebook_json(model['content'])
298 nb = current.to_notebook_json(model['content'])
299
299
300 self.check_and_sign(nb, new_path, new_name)
300 self.check_and_sign(nb, new_path, new_name)
301
301
302 if 'name' in nb['metadata']:
302 if 'name' in nb['metadata']:
303 nb['metadata']['name'] = u''
303 nb['metadata']['name'] = u''
304 try:
304 try:
305 self.log.debug("Autosaving notebook %s", os_path)
305 self.log.debug("Autosaving notebook %s", os_path)
306 with io.open(os_path, 'w', encoding='utf-8') as f:
306 with io.open(os_path, 'w', encoding='utf-8') as f:
307 current.write(nb, f, u'json')
307 current.write(nb, f, u'json')
308 except Exception as e:
308 except Exception as e:
309 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
309 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
310
310
311 # Save .py script as well
311 # Save .py script as well
312 if self.save_script:
312 if self.save_script:
313 py_path = os.path.splitext(os_path)[0] + '.py'
313 py_path = os.path.splitext(os_path)[0] + '.py'
314 self.log.debug("Writing script %s", py_path)
314 self.log.debug("Writing script %s", py_path)
315 try:
315 try:
316 with io.open(py_path, 'w', encoding='utf-8') as f:
316 with io.open(py_path, 'w', encoding='utf-8') as f:
317 current.write(nb, f, u'py')
317 current.write(nb, f, u'py')
318 except Exception as e:
318 except Exception as e:
319 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
319 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
320
320
321 model = self.get_notebook_model(new_name, new_path, content=False)
321 model = self.get_notebook_model(new_name, new_path, content=False)
322 return model
322 return model
323
323
324 def update_notebook_model(self, model, name, path=''):
324 def update_notebook_model(self, model, name, path=''):
325 """Update the notebook's path and/or name"""
325 """Update the notebook's path and/or name"""
326 path = path.strip('/')
326 path = path.strip('/')
327 new_name = model.get('name', name)
327 new_name = model.get('name', name)
328 new_path = model.get('path', path).strip('/')
328 new_path = model.get('path', path).strip('/')
329 if path != new_path or name != new_name:
329 if path != new_path or name != new_name:
330 self.rename_notebook(name, path, new_name, new_path)
330 self.rename_notebook(name, path, new_name, new_path)
331 model = self.get_notebook_model(new_name, new_path, content=False)
331 model = self.get_notebook_model(new_name, new_path, content=False)
332 return model
332 return model
333
333
334 def delete_notebook_model(self, name, path=''):
334 def delete_notebook_model(self, name, path=''):
335 """Delete notebook by name and path."""
335 """Delete notebook by name and path."""
336 path = path.strip('/')
336 path = path.strip('/')
337 os_path = self.get_os_path(name, path)
337 os_path = self.get_os_path(name, path)
338 if not os.path.isfile(os_path):
338 if not os.path.isfile(os_path):
339 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
339 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
340
340
341 # clear checkpoints
341 # clear checkpoints
342 for checkpoint in self.list_checkpoints(name, path):
342 for checkpoint in self.list_checkpoints(name, path):
343 checkpoint_id = checkpoint['id']
343 checkpoint_id = checkpoint['id']
344 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
344 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
345 if os.path.isfile(cp_path):
345 if os.path.isfile(cp_path):
346 self.log.debug("Unlinking checkpoint %s", cp_path)
346 self.log.debug("Unlinking checkpoint %s", cp_path)
347 os.unlink(cp_path)
347 os.unlink(cp_path)
348
348
349 self.log.debug("Unlinking notebook %s", os_path)
349 self.log.debug("Unlinking notebook %s", os_path)
350 os.unlink(os_path)
350 os.unlink(os_path)
351
351
352 def rename_notebook(self, old_name, old_path, new_name, new_path):
352 def rename_notebook(self, old_name, old_path, new_name, new_path):
353 """Rename a notebook."""
353 """Rename a notebook."""
354 old_path = old_path.strip('/')
354 old_path = old_path.strip('/')
355 new_path = new_path.strip('/')
355 new_path = new_path.strip('/')
356 if new_name == old_name and new_path == old_path:
356 if new_name == old_name and new_path == old_path:
357 return
357 return
358
358
359 new_os_path = self.get_os_path(new_name, new_path)
359 new_os_path = self.get_os_path(new_name, new_path)
360 old_os_path = self.get_os_path(old_name, old_path)
360 old_os_path = self.get_os_path(old_name, old_path)
361
361
362 # Should we proceed with the move?
362 # Should we proceed with the move?
363 if os.path.isfile(new_os_path):
363 if os.path.isfile(new_os_path):
364 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
364 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
365 if self.save_script:
365 if self.save_script:
366 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
366 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
367 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
367 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
368 if os.path.isfile(new_py_path):
368 if os.path.isfile(new_py_path):
369 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
369 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
370
370
371 # Move the notebook file
371 # Move the notebook file
372 try:
372 try:
373 os.rename(old_os_path, new_os_path)
373 os.rename(old_os_path, new_os_path)
374 except Exception as e:
374 except Exception as e:
375 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
375 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
376
376
377 # Move the checkpoints
377 # Move the checkpoints
378 old_checkpoints = self.list_checkpoints(old_name, old_path)
378 old_checkpoints = self.list_checkpoints(old_name, old_path)
379 for cp in old_checkpoints:
379 for cp in old_checkpoints:
380 checkpoint_id = cp['id']
380 checkpoint_id = cp['id']
381 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
381 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
382 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
382 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
383 if os.path.isfile(old_cp_path):
383 if os.path.isfile(old_cp_path):
384 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
384 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
385 os.rename(old_cp_path, new_cp_path)
385 os.rename(old_cp_path, new_cp_path)
386
386
387 # Move the .py script
387 # Move the .py script
388 if self.save_script:
388 if self.save_script:
389 os.rename(old_py_path, new_py_path)
389 os.rename(old_py_path, new_py_path)
390
390
391 # Checkpoint-related utilities
391 # Checkpoint-related utilities
392
392
393 def get_checkpoint_path(self, checkpoint_id, name, path=''):
393 def get_checkpoint_path(self, checkpoint_id, name, path=''):
394 """find the path to a checkpoint"""
394 """find the path to a checkpoint"""
395 path = path.strip('/')
395 path = path.strip('/')
396 basename, _ = os.path.splitext(name)
396 basename, _ = os.path.splitext(name)
397 filename = u"{name}-{checkpoint_id}{ext}".format(
397 filename = u"{name}-{checkpoint_id}{ext}".format(
398 name=basename,
398 name=basename,
399 checkpoint_id=checkpoint_id,
399 checkpoint_id=checkpoint_id,
400 ext=self.filename_ext,
400 ext=self.filename_ext,
401 )
401 )
402 cp_path = os.path.join(path, self.checkpoint_dir, filename)
402 cp_path = os.path.join(path, self.checkpoint_dir, filename)
403 return cp_path
403 return cp_path
404
404
405 def get_checkpoint_model(self, checkpoint_id, name, path=''):
405 def get_checkpoint_model(self, checkpoint_id, name, path=''):
406 """construct the info dict for a given checkpoint"""
406 """construct the info dict for a given checkpoint"""
407 path = path.strip('/')
407 path = path.strip('/')
408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
408 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
409 stats = os.stat(cp_path)
409 stats = os.stat(cp_path)
410 last_modified = tz.utcfromtimestamp(stats.st_mtime)
410 last_modified = tz.utcfromtimestamp(stats.st_mtime)
411 info = dict(
411 info = dict(
412 id = checkpoint_id,
412 id = checkpoint_id,
413 last_modified = last_modified,
413 last_modified = last_modified,
414 )
414 )
415 return info
415 return info
416
416
417 # public checkpoint API
417 # public checkpoint API
418
418
419 def create_checkpoint(self, name, path=''):
419 def create_checkpoint(self, name, path=''):
420 """Create a checkpoint from the current state of a notebook"""
420 """Create a checkpoint from the current state of a notebook"""
421 path = path.strip('/')
421 path = path.strip('/')
422 nb_path = self.get_os_path(name, path)
422 nb_path = self.get_os_path(name, path)
423 # only the one checkpoint ID:
423 # only the one checkpoint ID:
424 checkpoint_id = u"checkpoint"
424 checkpoint_id = u"checkpoint"
425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
425 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
426 self.log.debug("creating checkpoint for notebook %s", name)
426 self.log.debug("creating checkpoint for notebook %s", name)
427 if not os.path.exists(self.checkpoint_dir):
427 if not os.path.exists(self.checkpoint_dir):
428 os.mkdir(self.checkpoint_dir)
428 os.mkdir(self.checkpoint_dir)
429 shutil.copy2(nb_path, cp_path)
429 shutil.copy2(nb_path, cp_path)
430
430
431 # return the checkpoint info
431 # return the checkpoint info
432 return self.get_checkpoint_model(checkpoint_id, name, path)
432 return self.get_checkpoint_model(checkpoint_id, name, path)
433
433
434 def list_checkpoints(self, name, path=''):
434 def list_checkpoints(self, name, path=''):
435 """list the checkpoints for a given notebook
435 """list the checkpoints for a given notebook
436
436
437 This notebook manager currently only supports one checkpoint per notebook.
437 This notebook manager currently only supports one checkpoint per notebook.
438 """
438 """
439 path = path.strip('/')
439 path = path.strip('/')
440 checkpoint_id = "checkpoint"
440 checkpoint_id = "checkpoint"
441 path = self.get_checkpoint_path(checkpoint_id, name, path)
441 path = self.get_checkpoint_path(checkpoint_id, name, path)
442 if not os.path.exists(path):
442 if not os.path.exists(path):
443 return []
443 return []
444 else:
444 else:
445 return [self.get_checkpoint_model(checkpoint_id, name, path)]
445 return [self.get_checkpoint_model(checkpoint_id, name, path)]
446
446
447
447
448 def restore_checkpoint(self, checkpoint_id, name, path=''):
448 def restore_checkpoint(self, checkpoint_id, name, path=''):
449 """restore a notebook to a checkpointed state"""
449 """restore a notebook to a checkpointed state"""
450 path = path.strip('/')
450 path = path.strip('/')
451 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
451 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
452 nb_path = self.get_os_path(name, path)
452 nb_path = self.get_os_path(name, path)
453 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
453 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
454 if not os.path.isfile(cp_path):
454 if not os.path.isfile(cp_path):
455 self.log.debug("checkpoint file does not exist: %s", cp_path)
455 self.log.debug("checkpoint file does not exist: %s", cp_path)
456 raise web.HTTPError(404,
456 raise web.HTTPError(404,
457 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
457 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
458 )
458 )
459 # ensure notebook is readable (never restore from an unreadable notebook)
459 # ensure notebook is readable (never restore from an unreadable notebook)
460 with io.open(cp_path, 'r', encoding='utf-8') as f:
460 with io.open(cp_path, 'r', encoding='utf-8') as f:
461 nb = current.read(f, u'json')
461 nb = current.read(f, u'json')
462 shutil.copy2(cp_path, nb_path)
462 shutil.copy2(cp_path, nb_path)
463 self.log.debug("copying %s -> %s", cp_path, nb_path)
463 self.log.debug("copying %s -> %s", cp_path, nb_path)
464
464
465 def delete_checkpoint(self, checkpoint_id, name, path=''):
465 def delete_checkpoint(self, checkpoint_id, name, path=''):
466 """delete a notebook's checkpoint"""
466 """delete a notebook's checkpoint"""
467 path = path.strip('/')
467 path = path.strip('/')
468 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
468 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
469 if not os.path.isfile(cp_path):
469 if not os.path.isfile(cp_path):
470 raise web.HTTPError(404,
470 raise web.HTTPError(404,
471 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
471 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
472 )
472 )
473 self.log.debug("unlinking %s", cp_path)
473 self.log.debug("unlinking %s", cp_path)
474 os.unlink(cp_path)
474 os.unlink(cp_path)
475
475
476 def info_string(self):
476 def info_string(self):
477 return "Serving notebooks from local directory: %s" % self.notebook_dir
477 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,75 +1,76 b''
1 """Test HTML utils"""
1 """Test HTML utils"""
2
2
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2013 The IPython Development Team
4 # Copyright (C) 2013 The IPython Development Team
5 #
5 #
6 # Distributed under the terms of the BSD License. The full license is in
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING, distributed as part of this software.
7 # the file COPYING, distributed as part of this software.
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 import os
14 import os
15
15
16 import nose.tools as nt
16 import nose.tools as nt
17
17
18 import IPython.testing.tools as tt
18 import IPython.testing.tools as tt
19 from IPython.html.utils import url_escape, url_unescape, is_hidden
19 from IPython.html.utils import url_escape, url_unescape, is_hidden
20 from IPython.utils.tempdir import TemporaryDirectory
20 from IPython.utils.tempdir import TemporaryDirectory
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Test functions
23 # Test functions
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 def test_help_output():
26 def test_help_output():
27 """ipython notebook --help-all works"""
27 """ipython notebook --help-all works"""
28 tt.help_all_output_test('notebook')
28 tt.help_all_output_test('notebook')
29
29
30
30
31 def test_url_escape():
31 def test_url_escape():
32
32
33 # changes path or notebook name with special characters to url encoding
33 # changes path or notebook name with special characters to url encoding
34 # these tests specifically encode paths with spaces
34 # these tests specifically encode paths with spaces
35 path = url_escape('/this is a test/for spaces/')
35 path = url_escape('/this is a test/for spaces/')
36 nt.assert_equal(path, '/this%20is%20a%20test/for%20spaces/')
36 nt.assert_equal(path, '/this%20is%20a%20test/for%20spaces/')
37
37
38 path = url_escape('notebook with space.ipynb')
38 path = url_escape('notebook with space.ipynb')
39 nt.assert_equal(path, 'notebook%20with%20space.ipynb')
39 nt.assert_equal(path, 'notebook%20with%20space.ipynb')
40
40
41 path = url_escape('/path with a/notebook and space.ipynb')
41 path = url_escape('/path with a/notebook and space.ipynb')
42 nt.assert_equal(path, '/path%20with%20a/notebook%20and%20space.ipynb')
42 nt.assert_equal(path, '/path%20with%20a/notebook%20and%20space.ipynb')
43
43
44 path = url_escape('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
44 path = url_escape('/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
45 nt.assert_equal(path,
45 nt.assert_equal(path,
46 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
46 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
47
47
48 def test_url_unescape():
48 def test_url_unescape():
49
49
50 # decodes a url string to a plain string
50 # decodes a url string to a plain string
51 # these tests decode paths with spaces
51 # these tests decode paths with spaces
52 path = url_unescape('/this%20is%20a%20test/for%20spaces/')
52 path = url_unescape('/this%20is%20a%20test/for%20spaces/')
53 nt.assert_equal(path, '/this is a test/for spaces/')
53 nt.assert_equal(path, '/this is a test/for spaces/')
54
54
55 path = url_unescape('notebook%20with%20space.ipynb')
55 path = url_unescape('notebook%20with%20space.ipynb')
56 nt.assert_equal(path, 'notebook with space.ipynb')
56 nt.assert_equal(path, 'notebook with space.ipynb')
57
57
58 path = url_unescape('/path%20with%20a/notebook%20and%20space.ipynb')
58 path = url_unescape('/path%20with%20a/notebook%20and%20space.ipynb')
59 nt.assert_equal(path, '/path with a/notebook and space.ipynb')
59 nt.assert_equal(path, '/path with a/notebook and space.ipynb')
60
60
61 path = url_unescape(
61 path = url_unescape(
62 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
62 '/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
63 nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
63 nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
64
64
65 def test_is_hidden():
65 def test_is_hidden():
66 with TemporaryDirectory() as root:
66 with TemporaryDirectory() as root:
67 subdir1 = os.path.join(root, 'subdir')
67 subdir1 = os.path.join(root, 'subdir')
68 os.makedirs(subdir1)
68 os.makedirs(subdir1)
69 nt.assert_equal(is_hidden(root, subdir1), False)
69 nt.assert_equal(is_hidden(subdir1, root), False)
70 subdir2 = os.path.join(root, '.subdir2')
70 subdir2 = os.path.join(root, '.subdir2')
71 os.makedirs(subdir2)
71 os.makedirs(subdir2)
72 nt.assert_equal(is_hidden(root, subdir2), True)
72 nt.assert_equal(is_hidden(subdir2, root), True)
73 subdir34 = os.path.join(root, 'subdir3', '.subdir4')
73 subdir34 = os.path.join(root, 'subdir3', '.subdir4')
74 os.makedirs(subdir34)
74 os.makedirs(subdir34)
75 nt.assert_equal(is_hidden(root, subdir34), True)
75 nt.assert_equal(is_hidden(subdir34, root), True)
76 nt.assert_equal(is_hidden(subdir34), True)
@@ -1,107 +1,114 b''
1 """Notebook related utilities
1 """Notebook related utilities
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 from __future__ import print_function
15 from __future__ import print_function
16
16
17 import os
17 import os
18 import stat
18 import stat
19
19
20 try:
20 try:
21 from urllib.parse import quote, unquote
21 from urllib.parse import quote, unquote
22 except ImportError:
22 except ImportError:
23 from urllib import quote, unquote
23 from urllib import quote, unquote
24
24
25 from IPython.utils import py3compat
25 from IPython.utils import py3compat
26
26
27 # UF_HIDDEN is a stat flag not defined in the stat module.
27 # UF_HIDDEN is a stat flag not defined in the stat module.
28 # It is used by BSD to indicate hidden files.
28 # It is used by BSD to indicate hidden files.
29 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
29 UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Imports
32 # Imports
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34
34
35 def url_path_join(*pieces):
35 def url_path_join(*pieces):
36 """Join components of url into a relative url
36 """Join components of url into a relative url
37
37
38 Use to prevent double slash when joining subpath. This will leave the
38 Use to prevent double slash when joining subpath. This will leave the
39 initial and final / in place
39 initial and final / in place
40 """
40 """
41 initial = pieces[0].startswith('/')
41 initial = pieces[0].startswith('/')
42 final = pieces[-1].endswith('/')
42 final = pieces[-1].endswith('/')
43 stripped = [s.strip('/') for s in pieces]
43 stripped = [s.strip('/') for s in pieces]
44 result = '/'.join(s for s in stripped if s)
44 result = '/'.join(s for s in stripped if s)
45 if initial: result = '/' + result
45 if initial: result = '/' + result
46 if final: result = result + '/'
46 if final: result = result + '/'
47 if result == '//': result = '/'
47 if result == '//': result = '/'
48 return result
48 return result
49
49
50 def path2url(path):
50 def path2url(path):
51 """Convert a local file path to a URL"""
51 """Convert a local file path to a URL"""
52 pieces = [ quote(p) for p in path.split(os.sep) ]
52 pieces = [ quote(p) for p in path.split(os.sep) ]
53 # preserve trailing /
53 # preserve trailing /
54 if pieces[-1] == '':
54 if pieces[-1] == '':
55 pieces[-1] = '/'
55 pieces[-1] = '/'
56 url = url_path_join(*pieces)
56 url = url_path_join(*pieces)
57 return url
57 return url
58
58
59 def url2path(url):
59 def url2path(url):
60 """Convert a URL to a local file path"""
60 """Convert a URL to a local file path"""
61 pieces = [ unquote(p) for p in url.split('/') ]
61 pieces = [ unquote(p) for p in url.split('/') ]
62 path = os.path.join(*pieces)
62 path = os.path.join(*pieces)
63 return path
63 return path
64
64
65 def url_escape(path):
65 def url_escape(path):
66 """Escape special characters in a URL path
66 """Escape special characters in a URL path
67
67
68 Turns '/foo bar/' into '/foo%20bar/'
68 Turns '/foo bar/' into '/foo%20bar/'
69 """
69 """
70 parts = py3compat.unicode_to_str(path).split('/')
70 parts = py3compat.unicode_to_str(path).split('/')
71 return u'/'.join([quote(p) for p in parts])
71 return u'/'.join([quote(p) for p in parts])
72
72
73 def url_unescape(path):
73 def url_unescape(path):
74 """Unescape special characters in a URL path
74 """Unescape special characters in a URL path
75
75
76 Turns '/foo%20bar/' into '/foo bar/'
76 Turns '/foo%20bar/' into '/foo bar/'
77 """
77 """
78 return u'/'.join([
78 return u'/'.join([
79 py3compat.str_to_unicode(unquote(p))
79 py3compat.str_to_unicode(unquote(p))
80 for p in py3compat.unicode_to_str(path).split('/')
80 for p in py3compat.unicode_to_str(path).split('/')
81 ])
81 ])
82
82
83 def is_hidden(absolute_root, absolute_path):
83 def is_hidden(abs_path, abs_root=''):
84 """Is a file is hidden or contained in a hidden directory.
84 """Is a file is hidden or contained in a hidden directory.
85
85
86 Hidden is determined by either name starting with '.' or the UF_HIDDEN
86 This will start with the rightmost path element and work backwards to the
87 flag as reported by stat.
87 given root to see if a path is hidden or in a hidden directory. Hidden is
88 determined by either name starting with '.' or the UF_HIDDEN flag as
89 reported by stat.
88
90
89 Parameters
91 Parameters
90 ----------
92 ----------
91 absolute_root : unicode
93 abs_path : unicode
92 absolute_path : unicode
94 The absolute path to check for hidden directories.
95 abs_root : unicode
96 The absolute path of the root directory in which hidden directories
97 should be check for.
93 """
98 """
94 inside_root = absolute_path[len(absolute_root):]
99 if not abs_root:
100 abs_root = abs_path.split(os.sep, 1)[0] + os.sep
101 inside_root = abs_path[len(abs_root):]
95 if any(part.startswith('.') for part in inside_root.split(os.sep)):
102 if any(part.startswith('.') for part in inside_root.split(os.sep)):
96 return True
103 return True
97
104
98 # check UF_HIDDEN on any location up to root
105 # check UF_HIDDEN on any location up to root
99 path = absolute_path
106 path = abs_path
100 while path and path.startswith(absolute_root) and path != absolute_root:
107 while path and path.startswith(abs_root) and path != abs_root:
101 st = os.stat(path)
108 st = os.stat(path)
102 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
109 if getattr(st, 'st_flags', 0) & UF_HIDDEN:
103 return True
110 return True
104 path = os.path.dirname(path)
111 path = os.path.dirname(path)
105
112
106 return False
113 return False
107
114
General Comments 0
You need to be logged in to leave comments. Login now