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