##// END OF EJS Templates
Remove separate 'path', 'name' in Contents API...
MinRK -
Show More
@@ -298,7 +298,7 b' class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):'
298 @web.authenticated
298 @web.authenticated
299 def get(self, path):
299 def get(self, path):
300 if os.path.splitext(path)[1] == '.ipynb':
300 if os.path.splitext(path)[1] == '.ipynb':
301 name = os.path.basename(path)
301 name = path.rsplit('/', 1)[-1]
302 self.set_header('Content-Type', 'application/json')
302 self.set_header('Content-Type', 'application/json')
303 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
303 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
304
304
@@ -418,43 +418,42 b' class ApiVersionHandler(IPythonHandler):'
418 # not authenticated, so give as few info as possible
418 # not authenticated, so give as few info as possible
419 self.finish(json.dumps({"version":IPython.__version__}))
419 self.finish(json.dumps({"version":IPython.__version__}))
420
420
421
421 class TrailingSlashHandler(web.RequestHandler):
422 class TrailingSlashHandler(web.RequestHandler):
422 """Simple redirect handler that strips trailing slashes
423 """Simple redirect handler that strips trailing slashes
423
424
424 This should be the first, highest priority handler.
425 This should be the first, highest priority handler.
425 """
426 """
426
427
427 SUPPORTED_METHODS = ['GET']
428
429 def get(self):
428 def get(self):
430 self.redirect(self.request.uri.rstrip('/'))
429 self.redirect(self.request.uri.rstrip('/'))
430
431 post = put = get
431
432
432
433
433 class FilesRedirectHandler(IPythonHandler):
434 class FilesRedirectHandler(IPythonHandler):
434 """Handler for redirecting relative URLs to the /files/ handler"""
435 """Handler for redirecting relative URLs to the /files/ handler"""
435 def get(self, path=''):
436 def get(self, path=''):
436 cm = self.contents_manager
437 cm = self.contents_manager
437 if cm.path_exists(path):
438 if cm.dir_exists(path):
438 # it's a *directory*, redirect to /tree
439 # it's a *directory*, redirect to /tree
439 url = url_path_join(self.base_url, 'tree', path)
440 url = url_path_join(self.base_url, 'tree', path)
440 else:
441 else:
441 orig_path = path
442 orig_path = path
442 # otherwise, redirect to /files
443 # otherwise, redirect to /files
443 parts = path.split('/')
444 parts = path.split('/')
444 path = '/'.join(parts[:-1])
445 name = parts[-1]
446
445
447 if not cm.file_exists(name=name, path=path) and 'files' in parts:
446 if not cm.file_exists(path=path) and 'files' in parts:
448 # redirect without files/ iff it would 404
447 # redirect without files/ iff it would 404
449 # this preserves pre-2.0-style 'files/' links
448 # this preserves pre-2.0-style 'files/' links
450 self.log.warn("Deprecated files/ URL: %s", orig_path)
449 self.log.warn("Deprecated files/ URL: %s", orig_path)
451 parts.remove('files')
450 parts.remove('files')
452 path = '/'.join(parts[:-1])
451 path = '/'.join(parts)
453
452
454 if not cm.file_exists(name=name, path=path):
453 if not cm.file_exists(path=path):
455 raise web.HTTPError(404)
454 raise web.HTTPError(404)
456
455
457 url = url_path_join(self.base_url, 'files', path, name)
456 url = url_path_join(self.base_url, 'files', path)
458 url = url_escape(url)
457 url = url_escape(url)
459 self.log.debug("Redirecting %s to %s", self.request.path, url)
458 self.log.debug("Redirecting %s to %s", self.request.path, url)
460 self.redirect(url)
459 self.redirect(url)
@@ -464,11 +463,8 b' class FilesRedirectHandler(IPythonHandler):'
464 # URL pattern fragments for re-use
463 # URL pattern fragments for re-use
465 #-----------------------------------------------------------------------------
464 #-----------------------------------------------------------------------------
466
465
467 path_regex = r"(?P<path>(?:/.*)*)"
466 path_regex = r"(?P<path>.*)"
468 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
467 notebook_path_regex = r"(?P<path>.+\.ipynb)"
469 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
470 file_name_regex = r"(?P<name>[^/]+)"
471 file_path_regex = "%s/%s" % (path_regex, file_name_regex)
472
468
473 #-----------------------------------------------------------------------------
469 #-----------------------------------------------------------------------------
474 # URL to handler mappings
470 # URL to handler mappings
@@ -17,13 +17,18 b' class FilesHandler(IPythonHandler):'
17
17
18 @web.authenticated
18 @web.authenticated
19 def get(self, path):
19 def get(self, path):
20 cm = self.settings['contents_manager']
20 cm = self.contents_manager
21 if cm.is_hidden(path):
21 if cm.is_hidden(path):
22 self.log.info("Refusing to serve hidden file, via 404 Error")
22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 raise web.HTTPError(404)
23 raise web.HTTPError(404)
24
24
25 path, name = os.path.split(path)
25 path = path.strip('/')
26 model = cm.get_model(name, path)
26 if '/' in path:
27 _, name = path.rsplit('/', 1)
28 else:
29 name = path
30
31 model = cm.get_model(path)
27
32
28 if self.get_argument("download", False):
33 if self.get_argument("download", False):
29 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
34 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
@@ -76,12 +76,13 b' class NbconvertFileHandler(IPythonHandler):'
76 SUPPORTED_METHODS = ('GET',)
76 SUPPORTED_METHODS = ('GET',)
77
77
78 @web.authenticated
78 @web.authenticated
79 def get(self, format, path='', name=None):
79 def get(self, format, path):
80
80
81 exporter = get_exporter(format, config=self.config, log=self.log)
81 exporter = get_exporter(format, config=self.config, log=self.log)
82
82
83 path = path.strip('/')
83 path = path.strip('/')
84 model = self.contents_manager.get_model(name=name, path=path)
84 model = self.contents_manager.get_model(path=path)
85 name = model['name']
85
86
86 self.set_header('Last-Modified', model['last_modified'])
87 self.set_header('Last-Modified', model['last_modified'])
87
88
@@ -109,7 +110,7 b' class NbconvertFileHandler(IPythonHandler):'
109 class NbconvertPostHandler(IPythonHandler):
110 class NbconvertPostHandler(IPythonHandler):
110 SUPPORTED_METHODS = ('POST',)
111 SUPPORTED_METHODS = ('POST',)
111
112
112 @web.authenticated
113 @web.authenticated
113 def post(self, format):
114 def post(self, format):
114 exporter = get_exporter(format, config=self.config)
115 exporter = get_exporter(format, config=self.config)
115
116
@@ -17,18 +17,16 b' from ..utils import url_escape'
17 class NotebookHandler(IPythonHandler):
17 class NotebookHandler(IPythonHandler):
18
18
19 @web.authenticated
19 @web.authenticated
20 def get(self, path='', name=None):
20 def get(self, path=''):
21 """get renders the notebook template if a name is given, or
21 """get renders the notebook template if a name is given, or
22 redirects to the '/files/' handler if the name is not given."""
22 redirects to the '/files/' handler if the name is not given."""
23 path = path.strip('/')
23 path = path.strip('/')
24 cm = self.contents_manager
24 cm = self.contents_manager
25 if name is None:
26 raise web.HTTPError(500, "This shouldn't be accessible: %s" % self.request.uri)
27
25
28 # a .ipynb filename was given
26 # a .ipynb filename was given
29 if not cm.file_exists(name, path):
27 if not cm.file_exists(path):
30 raise web.HTTPError(404, u'Notebook does not exist: %s/%s' % (path, name))
28 raise web.HTTPError(404, u'Notebook does not exist: %s' % path)
31 name = url_escape(name)
29 name = url_escape(path.rsplit('/', 1)[-1])
32 path = url_escape(path)
30 path = url_escape(path)
33 self.write(self.render_template('notebook.html',
31 self.write(self.render_template('notebook.html',
34 notebook_path=path,
32 notebook_path=path,
@@ -189,7 +189,6 b' class NotebookWebApplication(web.Application):'
189 def init_handlers(self, settings):
189 def init_handlers(self, settings):
190 # Load the (URL pattern, handler) tuples for each component.
190 # Load the (URL pattern, handler) tuples for each component.
191 handlers = []
191 handlers = []
192 handlers.extend(load_handlers('base.handlers'))
193 handlers.extend(load_handlers('tree.handlers'))
192 handlers.extend(load_handlers('tree.handlers'))
194 handlers.extend(load_handlers('auth.login'))
193 handlers.extend(load_handlers('auth.login'))
195 handlers.extend(load_handlers('auth.logout'))
194 handlers.extend(load_handlers('auth.logout'))
@@ -206,6 +205,7 b' class NotebookWebApplication(web.Application):'
206 handlers.append(
205 handlers.append(
207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 )
207 )
208 handlers.extend(load_handlers('base.handlers'))
209 # set the URL that will be redirected from `/`
209 # set the URL that will be redirected from `/`
210 handlers.append(
210 handlers.append(
211 (r'/?', web.RedirectHandler, {
211 (r'/?', web.RedirectHandler, {
@@ -61,27 +61,23 b' class FileContentsManager(ContentsManager):'
61 except OSError as e:
61 except OSError as e:
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
62 self.log.debug("copystat on %s failed", dest, exc_info=True)
63
63
64 def _get_os_path(self, name=None, path=''):
64 def _get_os_path(self, path=''):
65 """Given a filename and API path, return its file system
65 """Given a filename and API path, return its file system
66 path.
66 path.
67
67
68 Parameters
68 Parameters
69 ----------
69 ----------
70 name : string
71 A filename
72 path : string
70 path : string
73 The relative API path to the named file.
71 The relative API path to the named file.
74
72
75 Returns
73 Returns
76 -------
74 -------
77 path : string
75 path : string
78 API path to be evaluated relative to root_dir.
76 Native, absolute OS path to for a file.
79 """
77 """
80 if name is not None:
81 path = url_path_join(path, name)
82 return to_os_path(path, self.root_dir)
78 return to_os_path(path, self.root_dir)
83
79
84 def path_exists(self, path):
80 def dir_exists(self, path):
85 """Does the API-style path refer to an extant directory?
81 """Does the API-style path refer to an extant directory?
86
82
87 API-style wrapper for os.path.isdir
83 API-style wrapper for os.path.isdir
@@ -112,25 +108,22 b' class FileContentsManager(ContentsManager):'
112
108
113 Returns
109 Returns
114 -------
110 -------
115 exists : bool
111 hidden : bool
116 Whether the path is hidden.
112 Whether the path exists and is hidden.
117
118 """
113 """
119 path = path.strip('/')
114 path = path.strip('/')
120 os_path = self._get_os_path(path=path)
115 os_path = self._get_os_path(path=path)
121 return is_hidden(os_path, self.root_dir)
116 return is_hidden(os_path, self.root_dir)
122
117
123 def file_exists(self, name, path=''):
118 def file_exists(self, path):
124 """Returns True if the file exists, else returns False.
119 """Returns True if the file exists, else returns False.
125
120
126 API-style wrapper for os.path.isfile
121 API-style wrapper for os.path.isfile
127
122
128 Parameters
123 Parameters
129 ----------
124 ----------
130 name : string
131 The name of the file you are checking.
132 path : string
125 path : string
133 The relative path to the file's directory (with '/' as separator)
126 The relative path to the file (with '/' as separator)
134
127
135 Returns
128 Returns
136 -------
129 -------
@@ -138,20 +131,18 b' class FileContentsManager(ContentsManager):'
138 Whether the file exists.
131 Whether the file exists.
139 """
132 """
140 path = path.strip('/')
133 path = path.strip('/')
141 nbpath = self._get_os_path(name, path=path)
134 nbpath = self._get_os_path(path)
142 return os.path.isfile(nbpath)
135 return os.path.isfile(nbpath)
143
136
144 def exists(self, name=None, path=''):
137 def exists(self, path):
145 """Returns True if the path [and name] exists, else returns False.
138 """Returns True if the path exists, else returns False.
146
139
147 API-style wrapper for os.path.exists
140 API-style wrapper for os.path.exists
148
141
149 Parameters
142 Parameters
150 ----------
143 ----------
151 name : string
152 The name of the file you are checking.
153 path : string
144 path : string
154 The relative path to the file's directory (with '/' as separator)
145 The API path to the file (with '/' as separator)
155
146
156 Returns
147 Returns
157 -------
148 -------
@@ -159,18 +150,18 b' class FileContentsManager(ContentsManager):'
159 Whether the target exists.
150 Whether the target exists.
160 """
151 """
161 path = path.strip('/')
152 path = path.strip('/')
162 os_path = self._get_os_path(name, path=path)
153 os_path = self._get_os_path(path=path)
163 return os.path.exists(os_path)
154 return os.path.exists(os_path)
164
155
165 def _base_model(self, name, path=''):
156 def _base_model(self, path):
166 """Build the common base of a contents model"""
157 """Build the common base of a contents model"""
167 os_path = self._get_os_path(name, path)
158 os_path = self._get_os_path(path)
168 info = os.stat(os_path)
159 info = os.stat(os_path)
169 last_modified = tz.utcfromtimestamp(info.st_mtime)
160 last_modified = tz.utcfromtimestamp(info.st_mtime)
170 created = tz.utcfromtimestamp(info.st_ctime)
161 created = tz.utcfromtimestamp(info.st_ctime)
171 # Create the base model.
162 # Create the base model.
172 model = {}
163 model = {}
173 model['name'] = name
164 model['name'] = path.rsplit('/', 1)[-1]
174 model['path'] = path
165 model['path'] = path
175 model['last_modified'] = last_modified
166 model['last_modified'] = last_modified
176 model['created'] = created
167 model['created'] = created
@@ -179,12 +170,12 b' class FileContentsManager(ContentsManager):'
179 model['message'] = None
170 model['message'] = None
180 return model
171 return model
181
172
182 def _dir_model(self, name, path='', content=True):
173 def _dir_model(self, path, content=True):
183 """Build a model for a directory
174 """Build a model for a directory
184
175
185 if content is requested, will include a listing of the directory
176 if content is requested, will include a listing of the directory
186 """
177 """
187 os_path = self._get_os_path(name, path)
178 os_path = self._get_os_path(path)
188
179
189 four_o_four = u'directory does not exist: %r' % os_path
180 four_o_four = u'directory does not exist: %r' % os_path
190
181
@@ -196,39 +187,36 b' class FileContentsManager(ContentsManager):'
196 )
187 )
197 raise web.HTTPError(404, four_o_four)
188 raise web.HTTPError(404, four_o_four)
198
189
199 if name is None:
190 model = self._base_model(path)
200 if '/' in path:
201 path, name = path.rsplit('/', 1)
202 else:
203 name = ''
204 model = self._base_model(name, path)
205 model['type'] = 'directory'
191 model['type'] = 'directory'
206 dir_path = u'{}/{}'.format(path, name)
207 if content:
192 if content:
208 model['content'] = contents = []
193 model['content'] = contents = []
209 for os_path in glob.glob(self._get_os_path('*', dir_path)):
194 for os_path in glob.glob(self._get_os_path('%s/*' % path)):
210 name = os.path.basename(os_path)
195 name = os.path.basename(os_path)
211 # skip over broken symlinks in listing
196 # skip over broken symlinks in listing
212 if not os.path.exists(os_path):
197 if not os.path.exists(os_path):
213 self.log.warn("%s doesn't exist", os_path)
198 self.log.warn("%s doesn't exist", os_path)
214 continue
199 continue
215 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
200 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
216 contents.append(self.get_model(name=name, path=dir_path, content=False))
201 contents.append(self.get_model(
202 path='%s/%s' % (path, name),
203 content=False)
204 )
217
205
218 model['format'] = 'json'
206 model['format'] = 'json'
219
207
220 return model
208 return model
221
209
222 def _file_model(self, name, path='', content=True):
210 def _file_model(self, path, content=True):
223 """Build a model for a file
211 """Build a model for a file
224
212
225 if content is requested, include the file contents.
213 if content is requested, include the file contents.
226 UTF-8 text files will be unicode, binary files will be base64-encoded.
214 UTF-8 text files will be unicode, binary files will be base64-encoded.
227 """
215 """
228 model = self._base_model(name, path)
216 model = self._base_model(path)
229 model['type'] = 'file'
217 model['type'] = 'file'
230 if content:
218 if content:
231 os_path = self._get_os_path(name, path)
219 os_path = self._get_os_path(path)
232 with io.open(os_path, 'rb') as f:
220 with io.open(os_path, 'rb') as f:
233 bcontent = f.read()
221 bcontent = f.read()
234 try:
222 try:
@@ -241,34 +229,32 b' class FileContentsManager(ContentsManager):'
241 return model
229 return model
242
230
243
231
244 def _notebook_model(self, name, path='', content=True):
232 def _notebook_model(self, path, content=True):
245 """Build a notebook model
233 """Build a notebook model
246
234
247 if content is requested, the notebook content will be populated
235 if content is requested, the notebook content will be populated
248 as a JSON structure (not double-serialized)
236 as a JSON structure (not double-serialized)
249 """
237 """
250 model = self._base_model(name, path)
238 model = self._base_model(path)
251 model['type'] = 'notebook'
239 model['type'] = 'notebook'
252 if content:
240 if content:
253 os_path = self._get_os_path(name, path)
241 os_path = self._get_os_path(path)
254 with io.open(os_path, 'r', encoding='utf-8') as f:
242 with io.open(os_path, 'r', encoding='utf-8') as f:
255 try:
243 try:
256 nb = nbformat.read(f, as_version=4)
244 nb = nbformat.read(f, as_version=4)
257 except Exception as e:
245 except Exception as e:
258 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
246 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
259 self.mark_trusted_cells(nb, name, path)
247 self.mark_trusted_cells(nb, path)
260 model['content'] = nb
248 model['content'] = nb
261 model['format'] = 'json'
249 model['format'] = 'json'
262 self.validate_notebook_model(model)
250 self.validate_notebook_model(model)
263 return model
251 return model
264
252
265 def get_model(self, name, path='', content=True):
253 def get_model(self, path, content=True):
266 """ Takes a path and name for an entity and returns its model
254 """ Takes a path for an entity and returns its model
267
255
268 Parameters
256 Parameters
269 ----------
257 ----------
270 name : str
271 the name of the target
272 path : str
258 path : str
273 the API path that describes the relative path for the target
259 the API path that describes the relative path for the target
274
260
@@ -280,32 +266,29 b' class FileContentsManager(ContentsManager):'
280 """
266 """
281 path = path.strip('/')
267 path = path.strip('/')
282
268
283 if not self.exists(name=name, path=path):
269 if not self.exists(path):
284 raise web.HTTPError(404, u'No such file or directory: %s/%s' % (path, name))
270 raise web.HTTPError(404, u'No such file or directory: %s' % path)
285
271
286 os_path = self._get_os_path(name, path)
272 os_path = self._get_os_path(path)
287 if os.path.isdir(os_path):
273 if os.path.isdir(os_path):
288 model = self._dir_model(name, path, content)
274 model = self._dir_model(path, content=content)
289 elif name.endswith('.ipynb'):
275 elif path.endswith('.ipynb'):
290 model = self._notebook_model(name, path, content)
276 model = self._notebook_model(path, content=content)
291 else:
277 else:
292 model = self._file_model(name, path, content)
278 model = self._file_model(path, content=content)
293 return model
279 return model
294
280
295 def _save_notebook(self, os_path, model, name='', path=''):
281 def _save_notebook(self, os_path, model, path=''):
296 """save a notebook file"""
282 """save a notebook file"""
297 # Save the notebook file
283 # Save the notebook file
298 nb = nbformat.from_dict(model['content'])
284 nb = nbformat.from_dict(model['content'])
299
285
300 self.check_and_sign(nb, name, path)
286 self.check_and_sign(nb, path)
301
302 if 'name' in nb['metadata']:
303 nb['metadata']['name'] = u''
304
287
305 with atomic_writing(os_path, encoding='utf-8') as f:
288 with atomic_writing(os_path, encoding='utf-8') as f:
306 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
289 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
307
290
308 def _save_file(self, os_path, model, name='', path=''):
291 def _save_file(self, os_path, model, path=''):
309 """save a non-notebook file"""
292 """save a non-notebook file"""
310 fmt = model.get('format', None)
293 fmt = model.get('format', None)
311 if fmt not in {'text', 'base64'}:
294 if fmt not in {'text', 'base64'}:
@@ -322,7 +305,7 b' class FileContentsManager(ContentsManager):'
322 with atomic_writing(os_path, text=False) as f:
305 with atomic_writing(os_path, text=False) as f:
323 f.write(bcontent)
306 f.write(bcontent)
324
307
325 def _save_directory(self, os_path, model, name='', path=''):
308 def _save_directory(self, os_path, model, path=''):
326 """create a directory"""
309 """create a directory"""
327 if is_hidden(os_path, self.root_dir):
310 if is_hidden(os_path, self.root_dir):
328 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
311 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
@@ -333,7 +316,7 b' class FileContentsManager(ContentsManager):'
333 else:
316 else:
334 self.log.debug("Directory %r already exists", os_path)
317 self.log.debug("Directory %r already exists", os_path)
335
318
336 def save(self, model, name='', path=''):
319 def save(self, model, path=''):
337 """Save the file model and return the model with no content."""
320 """Save the file model and return the model with no content."""
338 path = path.strip('/')
321 path = path.strip('/')
339
322
@@ -343,24 +326,18 b' class FileContentsManager(ContentsManager):'
343 raise web.HTTPError(400, u'No file content provided')
326 raise web.HTTPError(400, u'No file content provided')
344
327
345 # One checkpoint should always exist
328 # One checkpoint should always exist
346 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
329 if self.file_exists(path) and not self.list_checkpoints(path):
347 self.create_checkpoint(name, path)
330 self.create_checkpoint(path)
348
331
349 new_path = model.get('path', path).strip('/')
332 os_path = self._get_os_path(path)
350 new_name = model.get('name', name)
351
352 if path != new_path or name != new_name:
353 self.rename(name, path, new_name, new_path)
354
355 os_path = self._get_os_path(new_name, new_path)
356 self.log.debug("Saving %s", os_path)
333 self.log.debug("Saving %s", os_path)
357 try:
334 try:
358 if model['type'] == 'notebook':
335 if model['type'] == 'notebook':
359 self._save_notebook(os_path, model, new_name, new_path)
336 self._save_notebook(os_path, model, path)
360 elif model['type'] == 'file':
337 elif model['type'] == 'file':
361 self._save_file(os_path, model, new_name, new_path)
338 self._save_file(os_path, model, path)
362 elif model['type'] == 'directory':
339 elif model['type'] == 'directory':
363 self._save_directory(os_path, model, new_name, new_path)
340 self._save_directory(os_path, model, path)
364 else:
341 else:
365 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
342 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
366 except web.HTTPError:
343 except web.HTTPError:
@@ -373,29 +350,28 b' class FileContentsManager(ContentsManager):'
373 self.validate_notebook_model(model)
350 self.validate_notebook_model(model)
374 validation_message = model.get('message', None)
351 validation_message = model.get('message', None)
375
352
376 model = self.get_model(new_name, new_path, content=False)
353 model = self.get_model(path, content=False)
377 if validation_message:
354 if validation_message:
378 model['message'] = validation_message
355 model['message'] = validation_message
379 return model
356 return model
380
357
381 def update(self, model, name, path=''):
358 def update(self, model, path):
382 """Update the file's path and/or name
359 """Update the file's path
383
360
384 For use in PATCH requests, to enable renaming a file without
361 For use in PATCH requests, to enable renaming a file without
385 re-uploading its contents. Only used for renaming at the moment.
362 re-uploading its contents. Only used for renaming at the moment.
386 """
363 """
387 path = path.strip('/')
364 path = path.strip('/')
388 new_name = model.get('name', name)
389 new_path = model.get('path', path).strip('/')
365 new_path = model.get('path', path).strip('/')
390 if path != new_path or name != new_name:
366 if path != new_path:
391 self.rename(name, path, new_name, new_path)
367 self.rename(path, new_path)
392 model = self.get_model(new_name, new_path, content=False)
368 model = self.get_model(new_path, content=False)
393 return model
369 return model
394
370
395 def delete(self, name, path=''):
371 def delete(self, path):
396 """Delete file by name and path."""
372 """Delete file at path."""
397 path = path.strip('/')
373 path = path.strip('/')
398 os_path = self._get_os_path(name, path)
374 os_path = self._get_os_path(path)
399 rm = os.unlink
375 rm = os.unlink
400 if os.path.isdir(os_path):
376 if os.path.isdir(os_path):
401 listing = os.listdir(os_path)
377 listing = os.listdir(os_path)
@@ -406,9 +382,9 b' class FileContentsManager(ContentsManager):'
406 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
382 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
407
383
408 # clear checkpoints
384 # clear checkpoints
409 for checkpoint in self.list_checkpoints(name, path):
385 for checkpoint in self.list_checkpoints(path):
410 checkpoint_id = checkpoint['id']
386 checkpoint_id = checkpoint['id']
411 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
387 cp_path = self.get_checkpoint_path(checkpoint_id, path)
412 if os.path.isfile(cp_path):
388 if os.path.isfile(cp_path):
413 self.log.debug("Unlinking checkpoint %s", cp_path)
389 self.log.debug("Unlinking checkpoint %s", cp_path)
414 os.unlink(cp_path)
390 os.unlink(cp_path)
@@ -420,19 +396,19 b' class FileContentsManager(ContentsManager):'
420 self.log.debug("Unlinking file %s", os_path)
396 self.log.debug("Unlinking file %s", os_path)
421 rm(os_path)
397 rm(os_path)
422
398
423 def rename(self, old_name, old_path, new_name, new_path):
399 def rename(self, old_path, new_path):
424 """Rename a file."""
400 """Rename a file."""
425 old_path = old_path.strip('/')
401 old_path = old_path.strip('/')
426 new_path = new_path.strip('/')
402 new_path = new_path.strip('/')
427 if new_name == old_name and new_path == old_path:
403 if new_path == old_path:
428 return
404 return
429
405
430 new_os_path = self._get_os_path(new_name, new_path)
406 new_os_path = self._get_os_path(new_path)
431 old_os_path = self._get_os_path(old_name, old_path)
407 old_os_path = self._get_os_path(old_path)
432
408
433 # Should we proceed with the move?
409 # Should we proceed with the move?
434 if os.path.isfile(new_os_path):
410 if os.path.isfile(new_os_path):
435 raise web.HTTPError(409, u'File with name already exists: %s' % new_os_path)
411 raise web.HTTPError(409, u'File already exists: %s' % new_os_path)
436
412
437 # Move the file
413 # Move the file
438 try:
414 try:
@@ -441,36 +417,38 b' class FileContentsManager(ContentsManager):'
441 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
417 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_os_path, e))
442
418
443 # Move the checkpoints
419 # Move the checkpoints
444 old_checkpoints = self.list_checkpoints(old_name, old_path)
420 old_checkpoints = self.list_checkpoints(old_path)
445 for cp in old_checkpoints:
421 for cp in old_checkpoints:
446 checkpoint_id = cp['id']
422 checkpoint_id = cp['id']
447 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
423 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
448 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
424 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
449 if os.path.isfile(old_cp_path):
425 if os.path.isfile(old_cp_path):
450 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
426 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
451 shutil.move(old_cp_path, new_cp_path)
427 shutil.move(old_cp_path, new_cp_path)
452
428
453 # Checkpoint-related utilities
429 # Checkpoint-related utilities
454
430
455 def get_checkpoint_path(self, checkpoint_id, name, path=''):
431 def get_checkpoint_path(self, checkpoint_id, path):
456 """find the path to a checkpoint"""
432 """find the path to a checkpoint"""
457 path = path.strip('/')
433 path = path.strip('/')
434 parent, name = ('/' + path).rsplit('/', 1)
435 parent = parent.strip('/')
458 basename, ext = os.path.splitext(name)
436 basename, ext = os.path.splitext(name)
459 filename = u"{name}-{checkpoint_id}{ext}".format(
437 filename = u"{name}-{checkpoint_id}{ext}".format(
460 name=basename,
438 name=basename,
461 checkpoint_id=checkpoint_id,
439 checkpoint_id=checkpoint_id,
462 ext=ext,
440 ext=ext,
463 )
441 )
464 os_path = self._get_os_path(path=path)
442 os_path = self._get_os_path(path=parent)
465 cp_dir = os.path.join(os_path, self.checkpoint_dir)
443 cp_dir = os.path.join(os_path, self.checkpoint_dir)
466 ensure_dir_exists(cp_dir)
444 ensure_dir_exists(cp_dir)
467 cp_path = os.path.join(cp_dir, filename)
445 cp_path = os.path.join(cp_dir, filename)
468 return cp_path
446 return cp_path
469
447
470 def get_checkpoint_model(self, checkpoint_id, name, path=''):
448 def get_checkpoint_model(self, checkpoint_id, path):
471 """construct the info dict for a given checkpoint"""
449 """construct the info dict for a given checkpoint"""
472 path = path.strip('/')
450 path = path.strip('/')
473 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
451 cp_path = self.get_checkpoint_path(checkpoint_id, path)
474 stats = os.stat(cp_path)
452 stats = os.stat(cp_path)
475 last_modified = tz.utcfromtimestamp(stats.st_mtime)
453 last_modified = tz.utcfromtimestamp(stats.st_mtime)
476 info = dict(
454 info = dict(
@@ -481,43 +459,43 b' class FileContentsManager(ContentsManager):'
481
459
482 # public checkpoint API
460 # public checkpoint API
483
461
484 def create_checkpoint(self, name, path=''):
462 def create_checkpoint(self, path):
485 """Create a checkpoint from the current state of a file"""
463 """Create a checkpoint from the current state of a file"""
486 path = path.strip('/')
464 path = path.strip('/')
487 src_path = self._get_os_path(name, path)
465 src_path = self._get_os_path(path)
488 # only the one checkpoint ID:
466 # only the one checkpoint ID:
489 checkpoint_id = u"checkpoint"
467 checkpoint_id = u"checkpoint"
490 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
468 cp_path = self.get_checkpoint_path(checkpoint_id, path)
491 self.log.debug("creating checkpoint for %s", name)
469 self.log.debug("creating checkpoint for %s", path)
492 self._copy(src_path, cp_path)
470 self._copy(src_path, cp_path)
493
471
494 # return the checkpoint info
472 # return the checkpoint info
495 return self.get_checkpoint_model(checkpoint_id, name, path)
473 return self.get_checkpoint_model(checkpoint_id, path)
496
474
497 def list_checkpoints(self, name, path=''):
475 def list_checkpoints(self, path):
498 """list the checkpoints for a given file
476 """list the checkpoints for a given file
499
477
500 This contents manager currently only supports one checkpoint per file.
478 This contents manager currently only supports one checkpoint per file.
501 """
479 """
502 path = path.strip('/')
480 path = path.strip('/')
503 checkpoint_id = "checkpoint"
481 checkpoint_id = "checkpoint"
504 os_path = self.get_checkpoint_path(checkpoint_id, name, path)
482 os_path = self.get_checkpoint_path(checkpoint_id, path)
505 if not os.path.exists(os_path):
483 if not os.path.exists(os_path):
506 return []
484 return []
507 else:
485 else:
508 return [self.get_checkpoint_model(checkpoint_id, name, path)]
486 return [self.get_checkpoint_model(checkpoint_id, path)]
509
487
510
488
511 def restore_checkpoint(self, checkpoint_id, name, path=''):
489 def restore_checkpoint(self, checkpoint_id, path):
512 """restore a file to a checkpointed state"""
490 """restore a file to a checkpointed state"""
513 path = path.strip('/')
491 path = path.strip('/')
514 self.log.info("restoring %s from checkpoint %s", name, checkpoint_id)
492 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
515 nb_path = self._get_os_path(name, path)
493 nb_path = self._get_os_path(path)
516 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
494 cp_path = self.get_checkpoint_path(checkpoint_id, path)
517 if not os.path.isfile(cp_path):
495 if not os.path.isfile(cp_path):
518 self.log.debug("checkpoint file does not exist: %s", cp_path)
496 self.log.debug("checkpoint file does not exist: %s", cp_path)
519 raise web.HTTPError(404,
497 raise web.HTTPError(404,
520 u'checkpoint does not exist: %s-%s' % (name, checkpoint_id)
498 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
521 )
499 )
522 # ensure notebook is readable (never restore from an unreadable notebook)
500 # ensure notebook is readable (never restore from an unreadable notebook)
523 if cp_path.endswith('.ipynb'):
501 if cp_path.endswith('.ipynb'):
@@ -526,13 +504,13 b' class FileContentsManager(ContentsManager):'
526 self._copy(cp_path, nb_path)
504 self._copy(cp_path, nb_path)
527 self.log.debug("copying %s -> %s", cp_path, nb_path)
505 self.log.debug("copying %s -> %s", cp_path, nb_path)
528
506
529 def delete_checkpoint(self, checkpoint_id, name, path=''):
507 def delete_checkpoint(self, checkpoint_id, path):
530 """delete a file's checkpoint"""
508 """delete a file's checkpoint"""
531 path = path.strip('/')
509 path = path.strip('/')
532 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
510 cp_path = self.get_checkpoint_path(checkpoint_id, path)
533 if not os.path.isfile(cp_path):
511 if not os.path.isfile(cp_path):
534 raise web.HTTPError(404,
512 raise web.HTTPError(404,
535 u'Checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
513 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
536 )
514 )
537 self.log.debug("unlinking %s", cp_path)
515 self.log.debug("unlinking %s", cp_path)
538 os.unlink(cp_path)
516 os.unlink(cp_path)
@@ -540,6 +518,10 b' class FileContentsManager(ContentsManager):'
540 def info_string(self):
518 def info_string(self):
541 return "Serving notebooks from local directory: %s" % self.root_dir
519 return "Serving notebooks from local directory: %s" % self.root_dir
542
520
543 def get_kernel_path(self, name, path='', model=None):
521 def get_kernel_path(self, path, model=None):
544 """Return the initial working dir a kernel associated with a given notebook"""
522 """Return the initial working dir a kernel associated with a given notebook"""
545 return os.path.join(self.root_dir, path)
523 if '/' in path:
524 os_dir = path.rsplit('/', 1)[0]
525 else:
526 os_dir = ''
527 return self._get_os_path(os_dir)
@@ -10,9 +10,9 b' from tornado import web'
10 from IPython.html.utils import url_path_join, url_escape
10 from IPython.html.utils import url_path_join, url_escape
11 from IPython.utils.jsonutil import date_default
11 from IPython.utils.jsonutil import date_default
12
12
13 from IPython.html.base.handlers import (IPythonHandler, json_errors,
13 from IPython.html.base.handlers import (
14 file_path_regex, path_regex,
14 IPythonHandler, json_errors, path_regex,
15 file_name_regex)
15 )
16
16
17
17
18 def sort_key(model):
18 def sort_key(model):
@@ -29,38 +29,36 b' class ContentsHandler(IPythonHandler):'
29
29
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
30 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
31
31
32 def location_url(self, name, path):
32 def location_url(self, path):
33 """Return the full URL location of a file.
33 """Return the full URL location of a file.
34
34
35 Parameters
35 Parameters
36 ----------
36 ----------
37 name : unicode
38 The base name of the file, such as "foo.ipynb".
39 path : unicode
37 path : unicode
40 The API path of the file, such as "foo/bar".
38 The API path of the file, such as "foo/bar.txt".
41 """
39 """
42 return url_escape(url_path_join(
40 return url_escape(url_path_join(
43 self.base_url, 'api', 'contents', path, name
41 self.base_url, 'api', 'contents', path
44 ))
42 ))
45
43
46 def _finish_model(self, model, location=True):
44 def _finish_model(self, model, location=True):
47 """Finish a JSON request with a model, setting relevant headers, etc."""
45 """Finish a JSON request with a model, setting relevant headers, etc."""
48 if location:
46 if location:
49 location = self.location_url(model['name'], model['path'])
47 location = self.location_url(model['path'])
50 self.set_header('Location', location)
48 self.set_header('Location', location)
51 self.set_header('Last-Modified', model['last_modified'])
49 self.set_header('Last-Modified', model['last_modified'])
52 self.finish(json.dumps(model, default=date_default))
50 self.finish(json.dumps(model, default=date_default))
53
51
54 @web.authenticated
52 @web.authenticated
55 @json_errors
53 @json_errors
56 def get(self, path='', name=None):
54 def get(self, path=''):
57 """Return a model for a file or directory.
55 """Return a model for a file or directory.
58
56
59 A directory model contains a list of models (without content)
57 A directory model contains a list of models (without content)
60 of the files and directories it contains.
58 of the files and directories it contains.
61 """
59 """
62 path = path or ''
60 path = path or ''
63 model = self.contents_manager.get_model(name=name, path=path)
61 model = self.contents_manager.get_model(path=path)
64 if model['type'] == 'directory':
62 if model['type'] == 'directory':
65 # group listing by type, then by name (case-insensitive)
63 # group listing by type, then by name (case-insensitive)
66 # FIXME: sorting should be done in the frontends
64 # FIXME: sorting should be done in the frontends
@@ -69,91 +67,71 b' class ContentsHandler(IPythonHandler):'
69
67
70 @web.authenticated
68 @web.authenticated
71 @json_errors
69 @json_errors
72 def patch(self, path='', name=None):
70 def patch(self, path=''):
73 """PATCH renames a notebook without re-uploading content."""
71 """PATCH renames a file or directory without re-uploading content."""
74 cm = self.contents_manager
72 cm = self.contents_manager
75 if name is None:
76 raise web.HTTPError(400, u'Filename missing')
77 model = self.get_json_body()
73 model = self.get_json_body()
78 if model is None:
74 if model is None:
79 raise web.HTTPError(400, u'JSON body missing')
75 raise web.HTTPError(400, u'JSON body missing')
80 model = cm.update(model, name, path)
76 print('before', model)
77 model = cm.update(model, path)
78 print('after', model)
81 self._finish_model(model)
79 self._finish_model(model)
82
80
83 def _copy(self, copy_from, path, copy_to=None):
81 def _copy(self, copy_from, copy_to=None):
84 """Copy a file, optionally specifying the new name.
82 """Copy a file, optionally specifying the new path.
85 """
83 """
86 self.log.info(u"Copying {copy_from} to {path}/{copy_to}".format(
84 self.log.info(u"Copying {copy_from} to {copy_to}".format(
87 copy_from=copy_from,
85 copy_from=copy_from,
88 path=path,
89 copy_to=copy_to or '',
86 copy_to=copy_to or '',
90 ))
87 ))
91 model = self.contents_manager.copy(copy_from, copy_to, path)
88 model = self.contents_manager.copy(copy_from, copy_to)
92 self.set_status(201)
89 self.set_status(201)
93 self._finish_model(model)
90 self._finish_model(model)
94
91
95 def _upload(self, model, path, name=None):
92 def _upload(self, model, path):
96 """Handle upload of a new file
93 """Handle upload of a new file to path"""
97
94 self.log.info(u"Uploading file to %s", path)
98 If name specified, create it in path/name,
99 otherwise create a new untitled file in path.
100 """
101 self.log.info(u"Uploading file to %s/%s", path, name or '')
102 if name:
103 model['name'] = name
104
105 model = self.contents_manager.create_file(model, path)
95 model = self.contents_manager.create_file(model, path)
106 self.set_status(201)
96 self.set_status(201)
107 self._finish_model(model)
97 self._finish_model(model)
108
98
109 def _create_empty_file(self, path, name=None, ext='.ipynb'):
99 def _create_empty_file(self, path, ext='.ipynb'):
110 """Create an empty file in path
100 """Create an empty file in path
111
101
112 If name specified, create it in path/name.
102 If name specified, create it in path.
113 """
103 """
114 self.log.info(u"Creating new file in %s/%s", path, name or '')
104 self.log.info(u"Creating new file in %s", path)
115 model = {}
105 model = self.contents_manager.create_file(path=path, ext=ext)
116 if name:
117 model['name'] = name
118 model = self.contents_manager.create_file(model, path=path, ext=ext)
119 self.set_status(201)
106 self.set_status(201)
120 self._finish_model(model)
107 self._finish_model(model)
121
108
122 def _save(self, model, path, name):
109 def _save(self, model, path):
123 """Save an existing file."""
110 """Save an existing file."""
124 self.log.info(u"Saving file at %s/%s", path, name)
111 self.log.info(u"Saving file at %s", path)
125 model = self.contents_manager.save(model, name, path)
112 model = self.contents_manager.save(model, path)
126 if model['path'] != path.strip('/') or model['name'] != name:
113 self._finish_model(model)
127 # a rename happened, set Location header
128 location = True
129 else:
130 location = False
131 self._finish_model(model, location)
132
114
133 @web.authenticated
115 @web.authenticated
134 @json_errors
116 @json_errors
135 def post(self, path='', name=None):
117 def post(self, path=''):
136 """Create a new file or directory in the specified path.
118 """Create a new file or directory in the specified path.
137
119
138 POST creates new files or directories. The server always decides on the name.
120 POST creates new files or directories. The server always decides on the name.
139
121
140 POST /api/contents/path
122 POST /api/contents/path
141 New untitled notebook in path. If content specified, upload a
123 New untitled, empty file or directory.
142 notebook, otherwise start empty.
143 POST /api/contents/path
124 POST /api/contents/path
144 with body {"copy_from" : "OtherNotebook.ipynb"}
125 with body {"copy_from" : "/path/to/OtherNotebook.ipynb"}
145 New copy of OtherNotebook in path
126 New copy of OtherNotebook in path
146 """
127 """
147
128
148 if name is not None:
149 path = u'{}/{}'.format(path, name)
150
151 cm = self.contents_manager
129 cm = self.contents_manager
152
130
153 if cm.file_exists(path):
131 if cm.file_exists(path):
154 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
132 raise web.HTTPError(400, "Cannot POST to existing files, use PUT instead.")
155
133
156 if not cm.path_exists(path):
134 if not cm.dir_exists(path):
157 raise web.HTTPError(404, "No such directory: %s" % path)
135 raise web.HTTPError(404, "No such directory: %s" % path)
158
136
159 model = self.get_json_body()
137 model = self.get_json_body()
@@ -161,11 +139,7 b' class ContentsHandler(IPythonHandler):'
161 if model is not None:
139 if model is not None:
162 copy_from = model.get('copy_from')
140 copy_from = model.get('copy_from')
163 ext = model.get('ext', '.ipynb')
141 ext = model.get('ext', '.ipynb')
164 if model.get('content') is not None:
142 if copy_from:
165 if copy_from:
166 raise web.HTTPError(400, "Can't upload and copy at the same time.")
167 self._upload(model, path)
168 elif copy_from:
169 self._copy(copy_from, path)
143 self._copy(copy_from, path)
170 else:
144 else:
171 self._create_empty_file(path, ext=ext)
145 self._create_empty_file(path, ext=ext)
@@ -174,7 +148,7 b' class ContentsHandler(IPythonHandler):'
174
148
175 @web.authenticated
149 @web.authenticated
176 @json_errors
150 @json_errors
177 def put(self, path='', name=None):
151 def put(self, path=''):
178 """Saves the file in the location specified by name and path.
152 """Saves the file in the location specified by name and path.
179
153
180 PUT is very similar to POST, but the requester specifies the name,
154 PUT is very similar to POST, but the requester specifies the name,
@@ -193,30 +167,27 b' class ContentsHandler(IPythonHandler):'
193
167
194 Copy OtherNotebook to Name
168 Copy OtherNotebook to Name
195 """
169 """
196 if name is None:
197 raise web.HTTPError(400, "name must be specified with PUT.")
198
199 model = self.get_json_body()
170 model = self.get_json_body()
200 if model:
171 if model:
201 copy_from = model.get('copy_from')
172 copy_from = model.get('copy_from')
202 if copy_from:
173 if copy_from:
203 if model.get('content'):
174 if model.get('content'):
204 raise web.HTTPError(400, "Can't upload and copy at the same time.")
175 raise web.HTTPError(400, "Can't upload and copy at the same time.")
205 self._copy(copy_from, path, name)
176 self._copy(copy_from, path)
206 elif self.contents_manager.file_exists(name, path):
177 elif self.contents_manager.file_exists(path):
207 self._save(model, path, name)
178 self._save(model, path)
208 else:
179 else:
209 self._upload(model, path, name)
180 self._upload(model, path)
210 else:
181 else:
211 self._create_empty_file(path, name)
182 self._create_empty_file(path)
212
183
213 @web.authenticated
184 @web.authenticated
214 @json_errors
185 @json_errors
215 def delete(self, path='', name=None):
186 def delete(self, path=''):
216 """delete a file in the given path"""
187 """delete a file in the given path"""
217 cm = self.contents_manager
188 cm = self.contents_manager
218 self.log.warn('delete %s:%s', path, name)
189 self.log.warn('delete %s', path)
219 cm.delete(name, path)
190 cm.delete(path)
220 self.set_status(204)
191 self.set_status(204)
221 self.finish()
192 self.finish()
222
193
@@ -227,22 +198,22 b' class CheckpointsHandler(IPythonHandler):'
227
198
228 @web.authenticated
199 @web.authenticated
229 @json_errors
200 @json_errors
230 def get(self, path='', name=None):
201 def get(self, path=''):
231 """get lists checkpoints for a file"""
202 """get lists checkpoints for a file"""
232 cm = self.contents_manager
203 cm = self.contents_manager
233 checkpoints = cm.list_checkpoints(name, path)
204 checkpoints = cm.list_checkpoints(path)
234 data = json.dumps(checkpoints, default=date_default)
205 data = json.dumps(checkpoints, default=date_default)
235 self.finish(data)
206 self.finish(data)
236
207
237 @web.authenticated
208 @web.authenticated
238 @json_errors
209 @json_errors
239 def post(self, path='', name=None):
210 def post(self, path=''):
240 """post creates a new checkpoint"""
211 """post creates a new checkpoint"""
241 cm = self.contents_manager
212 cm = self.contents_manager
242 checkpoint = cm.create_checkpoint(name, path)
213 checkpoint = cm.create_checkpoint(path)
243 data = json.dumps(checkpoint, default=date_default)
214 data = json.dumps(checkpoint, default=date_default)
244 location = url_path_join(self.base_url, 'api/contents',
215 location = url_path_join(self.base_url, 'api/contents',
245 path, name, 'checkpoints', checkpoint['id'])
216 path, 'checkpoints', checkpoint['id'])
246 self.set_header('Location', url_escape(location))
217 self.set_header('Location', url_escape(location))
247 self.set_status(201)
218 self.set_status(201)
248 self.finish(data)
219 self.finish(data)
@@ -254,19 +225,19 b' class ModifyCheckpointsHandler(IPythonHandler):'
254
225
255 @web.authenticated
226 @web.authenticated
256 @json_errors
227 @json_errors
257 def post(self, path, name, checkpoint_id):
228 def post(self, path, checkpoint_id):
258 """post restores a file from a checkpoint"""
229 """post restores a file from a checkpoint"""
259 cm = self.contents_manager
230 cm = self.contents_manager
260 cm.restore_checkpoint(checkpoint_id, name, path)
231 cm.restore_checkpoint(checkpoint_id, path)
261 self.set_status(204)
232 self.set_status(204)
262 self.finish()
233 self.finish()
263
234
264 @web.authenticated
235 @web.authenticated
265 @json_errors
236 @json_errors
266 def delete(self, path, name, checkpoint_id):
237 def delete(self, path, checkpoint_id):
267 """delete clears a checkpoint for a given file"""
238 """delete clears a checkpoint for a given file"""
268 cm = self.contents_manager
239 cm = self.contents_manager
269 cm.delete_checkpoint(checkpoint_id, name, path)
240 cm.delete_checkpoint(checkpoint_id, path)
270 self.set_status(204)
241 self.set_status(204)
271 self.finish()
242 self.finish()
272
243
@@ -294,10 +265,9 b' class NotebooksRedirectHandler(IPythonHandler):'
294 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
265 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
295
266
296 default_handlers = [
267 default_handlers = [
297 (r"/api/contents%s/checkpoints" % file_path_regex, CheckpointsHandler),
268 (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler),
298 (r"/api/contents%s/checkpoints/%s" % (file_path_regex, _checkpoint_id_regex),
269 (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex),
299 ModifyCheckpointsHandler),
270 ModifyCheckpointsHandler),
300 (r"/api/contents%s" % file_path_regex, ContentsHandler),
301 (r"/api/contents%s" % path_regex, ContentsHandler),
271 (r"/api/contents%s" % path_regex, ContentsHandler),
302 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
272 (r"/api/notebooks/?(.*)", NotebooksRedirectHandler),
303 ]
273 ]
@@ -33,14 +33,6 b' class ContentsManager(LoggingConfigurable):'
33 - if unspecified, path defaults to '',
33 - if unspecified, path defaults to '',
34 indicating the root path.
34 indicating the root path.
35
35
36 name is also unicode, and refers to a specfic target:
37
38 - unicode, not url-escaped
39 - must not contain '/'
40 - It refers to an individual filename
41 - It may refer to a directory name,
42 in the case of listing or creating directories.
43
44 """
36 """
45
37
46 notary = Instance(sign.NotebookNotary)
38 notary = Instance(sign.NotebookNotary)
@@ -69,7 +61,7 b' class ContentsManager(LoggingConfigurable):'
69 # ContentsManager API part 1: methods that must be
61 # ContentsManager API part 1: methods that must be
70 # implemented in subclasses.
62 # implemented in subclasses.
71
63
72 def path_exists(self, path):
64 def dir_exists(self, path):
73 """Does the API-style path (directory) actually exist?
65 """Does the API-style path (directory) actually exist?
74
66
75 Like os.path.isdir
67 Like os.path.isdir
@@ -105,8 +97,8 b' class ContentsManager(LoggingConfigurable):'
105 """
97 """
106 raise NotImplementedError
98 raise NotImplementedError
107
99
108 def file_exists(self, name, path=''):
100 def file_exists(self, path=''):
109 """Does a file exist at the given name and path?
101 """Does a file exist at the given path?
110
102
111 Like os.path.isfile
103 Like os.path.isfile
112
104
@@ -126,15 +118,13 b' class ContentsManager(LoggingConfigurable):'
126 """
118 """
127 raise NotImplementedError('must be implemented in a subclass')
119 raise NotImplementedError('must be implemented in a subclass')
128
120
129 def exists(self, name, path=''):
121 def exists(self, path):
130 """Does a file or directory exist at the given name and path?
122 """Does a file or directory exist at the given name and path?
131
123
132 Like os.path.exists
124 Like os.path.exists
133
125
134 Parameters
126 Parameters
135 ----------
127 ----------
136 name : string
137 The name of the file you are checking.
138 path : string
128 path : string
139 The relative path to the file's directory (with '/' as separator)
129 The relative path to the file's directory (with '/' as separator)
140
130
@@ -143,17 +133,17 b' class ContentsManager(LoggingConfigurable):'
143 exists : bool
133 exists : bool
144 Whether the target exists.
134 Whether the target exists.
145 """
135 """
146 return self.file_exists(name, path) or self.path_exists("%s/%s" % (path, name))
136 return self.file_exists(path) or self.dir_exists(path)
147
137
148 def get_model(self, name, path='', content=True):
138 def get_model(self, path, content=True):
149 """Get the model of a file or directory with or without content."""
139 """Get the model of a file or directory with or without content."""
150 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
151
141
152 def save(self, model, name, path=''):
142 def save(self, model, path):
153 """Save the file or directory and return the model with no content."""
143 """Save the file or directory and return the model with no content."""
154 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
155
145
156 def update(self, model, name, path=''):
146 def update(self, model, path):
157 """Update the file or directory and return the model with no content.
147 """Update the file or directory and return the model with no content.
158
148
159 For use in PATCH requests, to enable renaming a file without
149 For use in PATCH requests, to enable renaming a file without
@@ -161,26 +151,26 b' class ContentsManager(LoggingConfigurable):'
161 """
151 """
162 raise NotImplementedError('must be implemented in a subclass')
152 raise NotImplementedError('must be implemented in a subclass')
163
153
164 def delete(self, name, path=''):
154 def delete(self, path):
165 """Delete file or directory by name and path."""
155 """Delete file or directory by path."""
166 raise NotImplementedError('must be implemented in a subclass')
156 raise NotImplementedError('must be implemented in a subclass')
167
157
168 def create_checkpoint(self, name, path=''):
158 def create_checkpoint(self, path):
169 """Create a checkpoint of the current state of a file
159 """Create a checkpoint of the current state of a file
170
160
171 Returns a checkpoint_id for the new checkpoint.
161 Returns a checkpoint_id for the new checkpoint.
172 """
162 """
173 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
174
164
175 def list_checkpoints(self, name, path=''):
165 def list_checkpoints(self, path):
176 """Return a list of checkpoints for a given file"""
166 """Return a list of checkpoints for a given file"""
177 return []
167 return []
178
168
179 def restore_checkpoint(self, checkpoint_id, name, path=''):
169 def restore_checkpoint(self, checkpoint_id, path):
180 """Restore a file from one of its checkpoints"""
170 """Restore a file from one of its checkpoints"""
181 raise NotImplementedError("must be implemented in a subclass")
171 raise NotImplementedError("must be implemented in a subclass")
182
172
183 def delete_checkpoint(self, checkpoint_id, name, path=''):
173 def delete_checkpoint(self, checkpoint_id, path):
184 """delete a checkpoint for a file"""
174 """delete a checkpoint for a file"""
185 raise NotImplementedError("must be implemented in a subclass")
175 raise NotImplementedError("must be implemented in a subclass")
186
176
@@ -190,7 +180,7 b' class ContentsManager(LoggingConfigurable):'
190 def info_string(self):
180 def info_string(self):
191 return "Serving contents"
181 return "Serving contents"
192
182
193 def get_kernel_path(self, name, path='', model=None):
183 def get_kernel_path(self, path, model=None):
194 """ Return the path to start kernel in """
184 """ Return the path to start kernel in """
195 return path
185 return path
196
186
@@ -214,7 +204,7 b' class ContentsManager(LoggingConfigurable):'
214 for i in itertools.count():
204 for i in itertools.count():
215 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
205 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
216 ext=ext)
206 ext=ext)
217 if not self.file_exists(name, path):
207 if not self.file_exists('{}/{}'.format(path, name)):
218 break
208 break
219 return name
209 return name
220
210
@@ -233,6 +223,8 b' class ContentsManager(LoggingConfigurable):'
233 path = path.strip('/')
223 path = path.strip('/')
234 if model is None:
224 if model is None:
235 model = {}
225 model = {}
226 else:
227 model.pop('path', None)
236 if 'content' not in model and model.get('type', None) != 'directory':
228 if 'content' not in model and model.get('type', None) != 'directory':
237 if ext == '.ipynb':
229 if ext == '.ipynb':
238 model['content'] = new_notebook()
230 model['content'] = new_notebook()
@@ -242,7 +234,7 b' class ContentsManager(LoggingConfigurable):'
242 model['content'] = ''
234 model['content'] = ''
243 model['type'] = 'file'
235 model['type'] = 'file'
244 model['format'] = 'text'
236 model['format'] = 'text'
245 if 'name' not in model:
237 if self.dir_exists(path):
246 if model['type'] == 'directory':
238 if model['type'] == 'directory':
247 untitled = self.untitled_directory
239 untitled = self.untitled_directory
248 elif model['type'] == 'notebook':
240 elif model['type'] == 'notebook':
@@ -251,13 +243,13 b' class ContentsManager(LoggingConfigurable):'
251 untitled = self.untitled_file
243 untitled = self.untitled_file
252 else:
244 else:
253 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
245 raise HTTPError(400, "Unexpected model type: %r" % model['type'])
254 model['name'] = self.increment_filename(untitled + ext, path)
246
255
247 name = self.increment_filename(untitled + ext, path)
256 model['path'] = path
248 path = '{0}/{1}'.format(path, name)
257 model = self.save(model, model['name'], model['path'])
249 model = self.save(model, path)
258 return model
250 return model
259
251
260 def copy(self, from_name, to_name=None, path=''):
252 def copy(self, from_path, to_path=None):
261 """Copy an existing file and return its new model.
253 """Copy an existing file and return its new model.
262
254
263 If to_name not specified, increment `from_name-Copy#.ext`.
255 If to_name not specified, increment `from_name-Copy#.ext`.
@@ -265,43 +257,48 b' class ContentsManager(LoggingConfigurable):'
265 copy_from can be a full path to a file,
257 copy_from can be a full path to a file,
266 or just a base name. If a base name, `path` is used.
258 or just a base name. If a base name, `path` is used.
267 """
259 """
268 path = path.strip('/')
260 path = from_path.strip('/')
269 if '/' in from_name:
261 if '/' in path:
270 from_path, from_name = from_name.rsplit('/', 1)
262 from_dir, from_name = path.rsplit('/', 1)
271 else:
263 else:
272 from_path = path
264 from_dir = ''
273 model = self.get_model(from_name, from_path)
265 from_name = path
266
267 model = self.get_model(path)
268 model.pop('path', None)
269 model.pop('name', None)
274 if model['type'] == 'directory':
270 if model['type'] == 'directory':
275 raise HTTPError(400, "Can't copy directories")
271 raise HTTPError(400, "Can't copy directories")
276 if not to_name:
272
273 if not to_path:
274 to_path = from_dir
275 if self.dir_exists(to_path):
277 base, ext = os.path.splitext(from_name)
276 base, ext = os.path.splitext(from_name)
278 copy_name = u'{0}-Copy{1}'.format(base, ext)
277 copy_name = u'{0}-Copy{1}'.format(base, ext)
279 to_name = self.increment_filename(copy_name, path)
278 to_name = self.increment_filename(copy_name, to_path)
280 model['name'] = to_name
279 to_path = '{0}/{1}'.format(to_path, to_name)
281 model['path'] = path
280
282 model = self.save(model, to_name, path)
281 model = self.save(model, to_path)
283 return model
282 return model
284
283
285 def log_info(self):
284 def log_info(self):
286 self.log.info(self.info_string())
285 self.log.info(self.info_string())
287
286
288 def trust_notebook(self, name, path=''):
287 def trust_notebook(self, path):
289 """Explicitly trust a notebook
288 """Explicitly trust a notebook
290
289
291 Parameters
290 Parameters
292 ----------
291 ----------
293 name : string
294 The filename of the notebook
295 path : string
292 path : string
296 The notebook's directory
293 The path of a notebook
297 """
294 """
298 model = self.get_model(name, path)
295 model = self.get_model(path)
299 nb = model['content']
296 nb = model['content']
300 self.log.warn("Trusting notebook %s/%s", path, name)
297 self.log.warn("Trusting notebook %s", path)
301 self.notary.mark_cells(nb, True)
298 self.notary.mark_cells(nb, True)
302 self.save(model, name, path)
299 self.save(model, path)
303
300
304 def check_and_sign(self, nb, name='', path=''):
301 def check_and_sign(self, nb, path=''):
305 """Check for trusted cells, and sign the notebook.
302 """Check for trusted cells, and sign the notebook.
306
303
307 Called as a part of saving notebooks.
304 Called as a part of saving notebooks.
@@ -310,17 +307,15 b' class ContentsManager(LoggingConfigurable):'
310 ----------
307 ----------
311 nb : dict
308 nb : dict
312 The notebook dict
309 The notebook dict
313 name : string
314 The filename of the notebook (for logging)
315 path : string
310 path : string
316 The notebook's directory (for logging)
311 The notebook's path (for logging)
317 """
312 """
318 if self.notary.check_cells(nb):
313 if self.notary.check_cells(nb):
319 self.notary.sign(nb)
314 self.notary.sign(nb)
320 else:
315 else:
321 self.log.warn("Saving untrusted notebook %s/%s", path, name)
316 self.log.warn("Saving untrusted notebook %s", path)
322
317
323 def mark_trusted_cells(self, nb, name='', path=''):
318 def mark_trusted_cells(self, nb, path=''):
324 """Mark cells as trusted if the notebook signature matches.
319 """Mark cells as trusted if the notebook signature matches.
325
320
326 Called as a part of loading notebooks.
321 Called as a part of loading notebooks.
@@ -329,14 +324,12 b' class ContentsManager(LoggingConfigurable):'
329 ----------
324 ----------
330 nb : dict
325 nb : dict
331 The notebook object (in current nbformat)
326 The notebook object (in current nbformat)
332 name : string
333 The filename of the notebook (for logging)
334 path : string
327 path : string
335 The notebook's directory (for logging)
328 The notebook's directory (for logging)
336 """
329 """
337 trusted = self.notary.check_signature(nb)
330 trusted = self.notary.check_signature(nb)
338 if not trusted:
331 if not trusted:
339 self.log.warn("Notebook %s/%s is not trusted", path, name)
332 self.log.warn("Notebook %s is not trusted", path)
340 self.notary.mark_cells(nb, trusted)
333 self.notary.mark_cells(nb, trusted)
341
334
342 def should_list(self, name):
335 def should_list(self, name):
@@ -46,8 +46,8 b' class API(object):'
46 def list(self, path='/'):
46 def list(self, path='/'):
47 return self._req('GET', path)
47 return self._req('GET', path)
48
48
49 def read(self, name, path='/'):
49 def read(self, path):
50 return self._req('GET', url_path_join(path, name))
50 return self._req('GET', path)
51
51
52 def create_untitled(self, path='/', ext=None):
52 def create_untitled(self, path='/', ext=None):
53 body = None
53 body = None
@@ -55,47 +55,44 b' class API(object):'
55 body = json.dumps({'ext': ext})
55 body = json.dumps({'ext': ext})
56 return self._req('POST', path, body)
56 return self._req('POST', path, body)
57
57
58 def upload_untitled(self, body, path='/'):
59 return self._req('POST', path, body)
60
61 def copy_untitled(self, copy_from, path='/'):
58 def copy_untitled(self, copy_from, path='/'):
62 body = json.dumps({'copy_from':copy_from})
59 body = json.dumps({'copy_from':copy_from})
63 return self._req('POST', path, body)
60 return self._req('POST', path, body)
64
61
65 def create(self, name, path='/'):
62 def create(self, path='/'):
66 return self._req('PUT', url_path_join(path, name))
63 return self._req('PUT', path)
67
64
68 def upload(self, name, body, path='/'):
65 def upload(self, path, body):
69 return self._req('PUT', url_path_join(path, name), body)
66 return self._req('PUT', path, body)
70
67
71 def mkdir(self, name, path='/'):
68 def mkdir(self, path='/'):
72 return self._req('PUT', url_path_join(path, name), json.dumps({'type': 'directory'}))
69 return self._req('PUT', path, json.dumps({'type': 'directory'}))
73
70
74 def copy(self, copy_from, copy_to, path='/'):
71 def copy(self, copy_from, path):
75 body = json.dumps({'copy_from':copy_from})
72 body = json.dumps({'copy_from':copy_from})
76 return self._req('PUT', url_path_join(path, copy_to), body)
73 return self._req('PUT', path, body)
77
74
78 def save(self, name, body, path='/'):
75 def save(self, path, body):
79 return self._req('PUT', url_path_join(path, name), body)
76 return self._req('PUT', path, body)
80
77
81 def delete(self, name, path='/'):
78 def delete(self, path='/'):
82 return self._req('DELETE', url_path_join(path, name))
79 return self._req('DELETE', path)
83
80
84 def rename(self, name, path, new_name):
81 def rename(self, path, new_path):
85 body = json.dumps({'name': new_name})
82 body = json.dumps({'path': new_path})
86 return self._req('PATCH', url_path_join(path, name), body)
83 return self._req('PATCH', path, body)
87
84
88 def get_checkpoints(self, name, path):
85 def get_checkpoints(self, path):
89 return self._req('GET', url_path_join(path, name, 'checkpoints'))
86 return self._req('GET', url_path_join(path, 'checkpoints'))
90
87
91 def new_checkpoint(self, name, path):
88 def new_checkpoint(self, path):
92 return self._req('POST', url_path_join(path, name, 'checkpoints'))
89 return self._req('POST', url_path_join(path, 'checkpoints'))
93
90
94 def restore_checkpoint(self, name, path, checkpoint_id):
91 def restore_checkpoint(self, path, checkpoint_id):
95 return self._req('POST', url_path_join(path, name, 'checkpoints', checkpoint_id))
92 return self._req('POST', url_path_join(path, 'checkpoints', checkpoint_id))
96
93
97 def delete_checkpoint(self, name, path, checkpoint_id):
94 def delete_checkpoint(self, path, checkpoint_id):
98 return self._req('DELETE', url_path_join(path, name, 'checkpoints', checkpoint_id))
95 return self._req('DELETE', url_path_join(path, 'checkpoints', checkpoint_id))
99
96
100 class APITest(NotebookTestBase):
97 class APITest(NotebookTestBase):
101 """Test the kernels web service API"""
98 """Test the kernels web service API"""
@@ -131,8 +128,6 b' class APITest(NotebookTestBase):'
131 self.blob = os.urandom(100)
128 self.blob = os.urandom(100)
132 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
129 self.b64_blob = base64.encodestring(self.blob).decode('ascii')
133
130
134
135
136 for d in (self.dirs + self.hidden_dirs):
131 for d in (self.dirs + self.hidden_dirs):
137 d.replace('/', os.sep)
132 d.replace('/', os.sep)
138 if not os.path.isdir(pjoin(nbdir, d)):
133 if not os.path.isdir(pjoin(nbdir, d)):
@@ -178,12 +173,12 b' class APITest(NotebookTestBase):'
178 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
173 nbs = notebooks_only(self.api.list(u'/unicodΓ©/').json())
179 self.assertEqual(len(nbs), 1)
174 self.assertEqual(len(nbs), 1)
180 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
175 self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
181 self.assertEqual(nbs[0]['path'], u'unicodΓ©')
176 self.assertEqual(nbs[0]['path'], u'unicodΓ©/innonascii.ipynb')
182
177
183 nbs = notebooks_only(self.api.list('/foo/bar/').json())
178 nbs = notebooks_only(self.api.list('/foo/bar/').json())
184 self.assertEqual(len(nbs), 1)
179 self.assertEqual(len(nbs), 1)
185 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
180 self.assertEqual(nbs[0]['name'], 'baz.ipynb')
186 self.assertEqual(nbs[0]['path'], 'foo/bar')
181 self.assertEqual(nbs[0]['path'], 'foo/bar/baz.ipynb')
187
182
188 nbs = notebooks_only(self.api.list('foo').json())
183 nbs = notebooks_only(self.api.list('foo').json())
189 self.assertEqual(len(nbs), 4)
184 self.assertEqual(len(nbs), 4)
@@ -208,8 +203,10 b' class APITest(NotebookTestBase):'
208
203
209 def test_get_nb_contents(self):
204 def test_get_nb_contents(self):
210 for d, name in self.dirs_nbs:
205 for d, name in self.dirs_nbs:
211 nb = self.api.read('%s.ipynb' % name, d+'/').json()
206 path = url_path_join(d, name + '.ipynb')
207 nb = self.api.read(path).json()
212 self.assertEqual(nb['name'], u'%s.ipynb' % name)
208 self.assertEqual(nb['name'], u'%s.ipynb' % name)
209 self.assertEqual(nb['path'], path)
213 self.assertEqual(nb['type'], 'notebook')
210 self.assertEqual(nb['type'], 'notebook')
214 self.assertIn('content', nb)
211 self.assertIn('content', nb)
215 self.assertEqual(nb['format'], 'json')
212 self.assertEqual(nb['format'], 'json')
@@ -220,12 +217,14 b' class APITest(NotebookTestBase):'
220 def test_get_contents_no_such_file(self):
217 def test_get_contents_no_such_file(self):
221 # Name that doesn't exist - should be a 404
218 # Name that doesn't exist - should be a 404
222 with assert_http_error(404):
219 with assert_http_error(404):
223 self.api.read('q.ipynb', 'foo')
220 self.api.read('foo/q.ipynb')
224
221
225 def test_get_text_file_contents(self):
222 def test_get_text_file_contents(self):
226 for d, name in self.dirs_nbs:
223 for d, name in self.dirs_nbs:
227 model = self.api.read(u'%s.txt' % name, d+'/').json()
224 path = url_path_join(d, name + '.txt')
225 model = self.api.read(path).json()
228 self.assertEqual(model['name'], u'%s.txt' % name)
226 self.assertEqual(model['name'], u'%s.txt' % name)
227 self.assertEqual(model['path'], path)
229 self.assertIn('content', model)
228 self.assertIn('content', model)
230 self.assertEqual(model['format'], 'text')
229 self.assertEqual(model['format'], 'text')
231 self.assertEqual(model['type'], 'file')
230 self.assertEqual(model['type'], 'file')
@@ -233,12 +232,14 b' class APITest(NotebookTestBase):'
233
232
234 # Name that doesn't exist - should be a 404
233 # Name that doesn't exist - should be a 404
235 with assert_http_error(404):
234 with assert_http_error(404):
236 self.api.read('q.txt', 'foo')
235 self.api.read('foo/q.txt')
237
236
238 def test_get_binary_file_contents(self):
237 def test_get_binary_file_contents(self):
239 for d, name in self.dirs_nbs:
238 for d, name in self.dirs_nbs:
240 model = self.api.read(u'%s.blob' % name, d+'/').json()
239 path = url_path_join(d, name + '.blob')
240 model = self.api.read(path).json()
241 self.assertEqual(model['name'], u'%s.blob' % name)
241 self.assertEqual(model['name'], u'%s.blob' % name)
242 self.assertEqual(model['path'], path)
242 self.assertIn('content', model)
243 self.assertIn('content', model)
243 self.assertEqual(model['format'], 'base64')
244 self.assertEqual(model['format'], 'base64')
244 self.assertEqual(model['type'], 'file')
245 self.assertEqual(model['type'], 'file')
@@ -247,66 +248,59 b' class APITest(NotebookTestBase):'
247
248
248 # Name that doesn't exist - should be a 404
249 # Name that doesn't exist - should be a 404
249 with assert_http_error(404):
250 with assert_http_error(404):
250 self.api.read('q.txt', 'foo')
251 self.api.read('foo/q.txt')
251
252
252 def _check_created(self, resp, name, path, type='notebook'):
253 def _check_created(self, resp, path, type='notebook'):
253 self.assertEqual(resp.status_code, 201)
254 self.assertEqual(resp.status_code, 201)
254 location_header = py3compat.str_to_unicode(resp.headers['Location'])
255 location_header = py3compat.str_to_unicode(resp.headers['Location'])
255 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path, name)))
256 self.assertEqual(location_header, url_escape(url_path_join(u'/api/contents', path)))
256 rjson = resp.json()
257 rjson = resp.json()
257 self.assertEqual(rjson['name'], name)
258 self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1])
258 self.assertEqual(rjson['path'], path)
259 self.assertEqual(rjson['path'], path)
259 self.assertEqual(rjson['type'], type)
260 self.assertEqual(rjson['type'], type)
260 isright = os.path.isdir if type == 'directory' else os.path.isfile
261 isright = os.path.isdir if type == 'directory' else os.path.isfile
261 assert isright(pjoin(
262 assert isright(pjoin(
262 self.notebook_dir.name,
263 self.notebook_dir.name,
263 path.replace('/', os.sep),
264 path.replace('/', os.sep),
264 name,
265 ))
265 ))
266
266
267 def test_create_untitled(self):
267 def test_create_untitled(self):
268 resp = self.api.create_untitled(path=u'Γ₯ b')
268 resp = self.api.create_untitled(path=u'Γ₯ b')
269 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
269 self._check_created(resp, u'Γ₯ b/Untitled0.ipynb')
270
270
271 # Second time
271 # Second time
272 resp = self.api.create_untitled(path=u'Γ₯ b')
272 resp = self.api.create_untitled(path=u'Γ₯ b')
273 self._check_created(resp, 'Untitled1.ipynb', u'Γ₯ b')
273 self._check_created(resp, u'Γ₯ b/Untitled1.ipynb')
274
274
275 # And two directories down
275 # And two directories down
276 resp = self.api.create_untitled(path='foo/bar')
276 resp = self.api.create_untitled(path='foo/bar')
277 self._check_created(resp, 'Untitled0.ipynb', 'foo/bar')
277 self._check_created(resp, 'foo/bar/Untitled0.ipynb')
278
278
279 def test_create_untitled_txt(self):
279 def test_create_untitled_txt(self):
280 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
280 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
281 self._check_created(resp, 'untitled0.txt', 'foo/bar', type='file')
281 self._check_created(resp, 'foo/bar/untitled0.txt', type='file')
282
282
283 resp = self.api.read(path='foo/bar', name='untitled0.txt')
283 resp = self.api.read(path='foo/bar/untitled0.txt')
284 model = resp.json()
284 model = resp.json()
285 self.assertEqual(model['type'], 'file')
285 self.assertEqual(model['type'], 'file')
286 self.assertEqual(model['format'], 'text')
286 self.assertEqual(model['format'], 'text')
287 self.assertEqual(model['content'], '')
287 self.assertEqual(model['content'], '')
288
288
289 def test_upload_untitled(self):
290 nb = new_notebook()
291 nbmodel = {'content': nb, 'type': 'notebook'}
292 resp = self.api.upload_untitled(path=u'Γ₯ b',
293 body=json.dumps(nbmodel))
294 self._check_created(resp, 'Untitled0.ipynb', u'Γ₯ b')
295
296 def test_upload(self):
289 def test_upload(self):
297 nb = new_notebook()
290 nb = new_notebook()
298 nbmodel = {'content': nb, 'type': 'notebook'}
291 nbmodel = {'content': nb, 'type': 'notebook'}
299 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
292 path = u'Γ₯ b/Upload tΓ©st.ipynb'
300 body=json.dumps(nbmodel))
293 resp = self.api.upload(path, body=json.dumps(nbmodel))
301 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
294 self._check_created(resp, path)
302
295
303 def test_mkdir(self):
296 def test_mkdir(self):
304 resp = self.api.mkdir(u'New βˆ‚ir', path=u'Γ₯ b')
297 path = u'Γ₯ b/New βˆ‚ir'
305 self._check_created(resp, u'New βˆ‚ir', u'Γ₯ b', type='directory')
298 resp = self.api.mkdir(path)
299 self._check_created(resp, path, type='directory')
306
300
307 def test_mkdir_hidden_400(self):
301 def test_mkdir_hidden_400(self):
308 with assert_http_error(400):
302 with assert_http_error(400):
309 resp = self.api.mkdir(u'.hidden', path=u'Γ₯ b')
303 resp = self.api.mkdir(u'Γ₯ b/.hidden')
310
304
311 def test_upload_txt(self):
305 def test_upload_txt(self):
312 body = u'ΓΌnicode tΓ©xt'
306 body = u'ΓΌnicode tΓ©xt'
@@ -315,11 +309,11 b' class APITest(NotebookTestBase):'
315 'format' : 'text',
309 'format' : 'text',
316 'type' : 'file',
310 'type' : 'file',
317 }
311 }
318 resp = self.api.upload(u'Upload tΓ©st.txt', path=u'Γ₯ b',
312 path = u'Γ₯ b/Upload tΓ©st.txt'
319 body=json.dumps(model))
313 resp = self.api.upload(path, body=json.dumps(model))
320
314
321 # check roundtrip
315 # check roundtrip
322 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.txt')
316 resp = self.api.read(path)
323 model = resp.json()
317 model = resp.json()
324 self.assertEqual(model['type'], 'file')
318 self.assertEqual(model['type'], 'file')
325 self.assertEqual(model['format'], 'text')
319 self.assertEqual(model['format'], 'text')
@@ -333,13 +327,14 b' class APITest(NotebookTestBase):'
333 'format' : 'base64',
327 'format' : 'base64',
334 'type' : 'file',
328 'type' : 'file',
335 }
329 }
336 resp = self.api.upload(u'Upload tΓ©st.blob', path=u'Γ₯ b',
330 path = u'Γ₯ b/Upload tΓ©st.blob'
337 body=json.dumps(model))
331 resp = self.api.upload(path, body=json.dumps(model))
338
332
339 # check roundtrip
333 # check roundtrip
340 resp = self.api.read(path=u'Γ₯ b', name=u'Upload tΓ©st.blob')
334 resp = self.api.read(path)
341 model = resp.json()
335 model = resp.json()
342 self.assertEqual(model['type'], 'file')
336 self.assertEqual(model['type'], 'file')
337 self.assertEqual(model['path'], path)
343 self.assertEqual(model['format'], 'base64')
338 self.assertEqual(model['format'], 'base64')
344 decoded = base64.decodestring(model['content'].encode('ascii'))
339 decoded = base64.decodestring(model['content'].encode('ascii'))
345 self.assertEqual(decoded, body)
340 self.assertEqual(decoded, body)
@@ -350,24 +345,27 b' class APITest(NotebookTestBase):'
350 nb.worksheets.append(ws)
345 nb.worksheets.append(ws)
351 ws.cells.append(v2.new_code_cell(input='print("hi")'))
346 ws.cells.append(v2.new_code_cell(input='print("hi")'))
352 nbmodel = {'content': nb, 'type': 'notebook'}
347 nbmodel = {'content': nb, 'type': 'notebook'}
353 resp = self.api.upload(u'Upload tΓ©st.ipynb', path=u'Γ₯ b',
348 path = u'Γ₯ b/Upload tΓ©st.ipynb'
354 body=json.dumps(nbmodel))
349 resp = self.api.upload(path, body=json.dumps(nbmodel))
355 self._check_created(resp, u'Upload tΓ©st.ipynb', u'Γ₯ b')
350 self._check_created(resp, path)
356 resp = self.api.read(u'Upload tΓ©st.ipynb', u'Γ₯ b')
351 resp = self.api.read(path)
357 data = resp.json()
352 data = resp.json()
358 self.assertEqual(data['content']['nbformat'], 4)
353 self.assertEqual(data['content']['nbformat'], 4)
359
354
360 def test_copy_untitled(self):
355 def test_copy_untitled(self):
361 resp = self.api.copy_untitled(u'Γ§ d.ipynb', path=u'Γ₯ b')
356 resp = self.api.copy_untitled(u'Γ₯ b/Γ§ d.ipynb', u'unicodΓ©')
362 self._check_created(resp, u'Γ§ d-Copy0.ipynb', u'Γ₯ b')
357 self._check_created(resp, u'unicodΓ©/Γ§ d-Copy0.ipynb')
358
359 resp = self.api.copy_untitled(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b')
360 self._check_created(resp, u'Γ₯ b/Γ§ d-Copy0.ipynb')
363
361
364 def test_copy(self):
362 def test_copy(self):
365 resp = self.api.copy(u'Γ§ d.ipynb', u'cΓΈpy.ipynb', path=u'Γ₯ b')
363 resp = self.api.copy(u'Γ₯ b/Γ§ d.ipynb', u'Γ₯ b/cΓΈpy.ipynb')
366 self._check_created(resp, u'cΓΈpy.ipynb', u'Γ₯ b')
364 self._check_created(resp, u'Γ₯ b/cΓΈpy.ipynb')
367
365
368 def test_copy_path(self):
366 def test_copy_path(self):
369 resp = self.api.copy(u'foo/a.ipynb', u'cΓΈpyfoo.ipynb', path=u'Γ₯ b')
367 resp = self.api.copy(u'foo/a.ipynb', u'Γ₯ b/cΓΈpyfoo.ipynb')
370 self._check_created(resp, u'cΓΈpyfoo.ipynb', u'Γ₯ b')
368 self._check_created(resp, u'Γ₯ b/cΓΈpyfoo.ipynb')
371
369
372 def test_copy_dir_400(self):
370 def test_copy_dir_400(self):
373 # can't copy directories
371 # can't copy directories
@@ -376,19 +374,23 b' class APITest(NotebookTestBase):'
376
374
377 def test_delete(self):
375 def test_delete(self):
378 for d, name in self.dirs_nbs:
376 for d, name in self.dirs_nbs:
379 resp = self.api.delete('%s.ipynb' % name, d)
377 print('%r, %r' % (d, name))
378 resp = self.api.delete(url_path_join(d, name + '.ipynb'))
380 self.assertEqual(resp.status_code, 204)
379 self.assertEqual(resp.status_code, 204)
381
380
382 for d in self.dirs + ['/']:
381 for d in self.dirs + ['/']:
383 nbs = notebooks_only(self.api.list(d).json())
382 nbs = notebooks_only(self.api.list(d).json())
384 self.assertEqual(len(nbs), 0)
383 print('------')
384 print(d)
385 print(nbs)
386 self.assertEqual(nbs, [])
385
387
386 def test_delete_dirs(self):
388 def test_delete_dirs(self):
387 # depth-first delete everything, so we don't try to delete empty directories
389 # depth-first delete everything, so we don't try to delete empty directories
388 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
390 for name in sorted(self.dirs + ['/'], key=len, reverse=True):
389 listing = self.api.list(name).json()['content']
391 listing = self.api.list(name).json()['content']
390 for model in listing:
392 for model in listing:
391 self.api.delete(model['name'], model['path'])
393 self.api.delete(model['path'])
392 listing = self.api.list('/').json()['content']
394 listing = self.api.list('/').json()['content']
393 self.assertEqual(listing, [])
395 self.assertEqual(listing, [])
394
396
@@ -398,9 +400,10 b' class APITest(NotebookTestBase):'
398 self.api.delete(u'Γ₯ b')
400 self.api.delete(u'Γ₯ b')
399
401
400 def test_rename(self):
402 def test_rename(self):
401 resp = self.api.rename('a.ipynb', 'foo', 'z.ipynb')
403 resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb')
402 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
404 self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb')
403 self.assertEqual(resp.json()['name'], 'z.ipynb')
405 self.assertEqual(resp.json()['name'], 'z.ipynb')
406 self.assertEqual(resp.json()['path'], 'foo/z.ipynb')
404 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
407 assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
405
408
406 nbs = notebooks_only(self.api.list('foo').json())
409 nbs = notebooks_only(self.api.list('foo').json())
@@ -410,41 +413,31 b' class APITest(NotebookTestBase):'
410
413
411 def test_rename_existing(self):
414 def test_rename_existing(self):
412 with assert_http_error(409):
415 with assert_http_error(409):
413 self.api.rename('a.ipynb', 'foo', 'b.ipynb')
416 self.api.rename('foo/a.ipynb', 'foo/b.ipynb')
414
417
415 def test_save(self):
418 def test_save(self):
416 resp = self.api.read('a.ipynb', 'foo')
419 resp = self.api.read('foo/a.ipynb')
417 nbcontent = json.loads(resp.text)['content']
420 nbcontent = json.loads(resp.text)['content']
418 nb = from_dict(nbcontent)
421 nb = from_dict(nbcontent)
419 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
422 nb.cells.append(new_markdown_cell(u'Created by test Β³'))
420
423
421 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
424 nbmodel= {'content': nb, 'type': 'notebook'}
422 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
425 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
423
426
424 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
427 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
425 with io.open(nbfile, 'r', encoding='utf-8') as f:
428 with io.open(nbfile, 'r', encoding='utf-8') as f:
426 newnb = read(f, as_version=4)
429 newnb = read(f, as_version=4)
427 self.assertEqual(newnb.cells[0].source,
430 self.assertEqual(newnb.cells[0].source,
428 u'Created by test Β³')
431 u'Created by test Β³')
429 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
432 nbcontent = self.api.read('foo/a.ipynb').json()['content']
430 newnb = from_dict(nbcontent)
433 newnb = from_dict(nbcontent)
431 self.assertEqual(newnb.cells[0].source,
434 self.assertEqual(newnb.cells[0].source,
432 u'Created by test Β³')
435 u'Created by test Β³')
433
436
434 # Save and rename
435 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
436 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
437 saved = resp.json()
438 self.assertEqual(saved['name'], 'a2.ipynb')
439 self.assertEqual(saved['path'], 'foo/bar')
440 assert os.path.isfile(pjoin(self.notebook_dir.name,'foo','bar','a2.ipynb'))
441 assert not os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'a.ipynb'))
442 with assert_http_error(404):
443 self.api.read('a.ipynb', 'foo')
444
437
445 def test_checkpoints(self):
438 def test_checkpoints(self):
446 resp = self.api.read('a.ipynb', 'foo')
439 resp = self.api.read('foo/a.ipynb')
447 r = self.api.new_checkpoint('a.ipynb', 'foo')
440 r = self.api.new_checkpoint('foo/a.ipynb')
448 self.assertEqual(r.status_code, 201)
441 self.assertEqual(r.status_code, 201)
449 cp1 = r.json()
442 cp1 = r.json()
450 self.assertEqual(set(cp1), {'id', 'last_modified'})
443 self.assertEqual(set(cp1), {'id', 'last_modified'})
@@ -456,26 +449,26 b' class APITest(NotebookTestBase):'
456 hcell = new_markdown_cell('Created by test')
449 hcell = new_markdown_cell('Created by test')
457 nb.cells.append(hcell)
450 nb.cells.append(hcell)
458 # Save
451 # Save
459 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
452 nbmodel= {'content': nb, 'type': 'notebook'}
460 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
453 resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel))
461
454
462 # List checkpoints
455 # List checkpoints
463 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
456 cps = self.api.get_checkpoints('foo/a.ipynb').json()
464 self.assertEqual(cps, [cp1])
457 self.assertEqual(cps, [cp1])
465
458
466 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
459 nbcontent = self.api.read('foo/a.ipynb').json()['content']
467 nb = from_dict(nbcontent)
460 nb = from_dict(nbcontent)
468 self.assertEqual(nb.cells[0].source, 'Created by test')
461 self.assertEqual(nb.cells[0].source, 'Created by test')
469
462
470 # Restore cp1
463 # Restore cp1
471 r = self.api.restore_checkpoint('a.ipynb', 'foo', cp1['id'])
464 r = self.api.restore_checkpoint('foo/a.ipynb', cp1['id'])
472 self.assertEqual(r.status_code, 204)
465 self.assertEqual(r.status_code, 204)
473 nbcontent = self.api.read('a.ipynb', 'foo').json()['content']
466 nbcontent = self.api.read('foo/a.ipynb').json()['content']
474 nb = from_dict(nbcontent)
467 nb = from_dict(nbcontent)
475 self.assertEqual(nb.cells, [])
468 self.assertEqual(nb.cells, [])
476
469
477 # Delete cp1
470 # Delete cp1
478 r = self.api.delete_checkpoint('a.ipynb', 'foo', cp1['id'])
471 r = self.api.delete_checkpoint('foo/a.ipynb', cp1['id'])
479 self.assertEqual(r.status_code, 204)
472 self.assertEqual(r.status_code, 204)
480 cps = self.api.get_checkpoints('a.ipynb', 'foo').json()
473 cps = self.api.get_checkpoints('foo/a.ipynb').json()
481 self.assertEqual(cps, [])
474 self.assertEqual(cps, [])
@@ -42,7 +42,7 b' class TestFileContentsManager(TestCase):'
42 with TemporaryDirectory() as td:
42 with TemporaryDirectory() as td:
43 root = td
43 root = td
44 fm = FileContentsManager(root_dir=root)
44 fm = FileContentsManager(root_dir=root)
45 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
45 path = fm._get_os_path('/path/to/notebook/test.ipynb')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
46 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
47 fs_path = os.path.join(fm.root_dir, *rel_path_list)
48 self.assertEqual(path, fs_path)
48 self.assertEqual(path, fs_path)
@@ -53,7 +53,7 b' class TestFileContentsManager(TestCase):'
53 self.assertEqual(path, fs_path)
53 self.assertEqual(path, fs_path)
54
54
55 fm = FileContentsManager(root_dir=root)
55 fm = FileContentsManager(root_dir=root)
56 path = fm._get_os_path('test.ipynb', '////')
56 path = fm._get_os_path('////test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
57 fs_path = os.path.join(fm.root_dir, 'test.ipynb')
58 self.assertEqual(path, fs_path)
58 self.assertEqual(path, fs_path)
59
59
@@ -64,8 +64,8 b' class TestFileContentsManager(TestCase):'
64 root = td
64 root = td
65 os.mkdir(os.path.join(td, subd))
65 os.mkdir(os.path.join(td, subd))
66 fm = FileContentsManager(root_dir=root)
66 fm = FileContentsManager(root_dir=root)
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb', '/')
67 cp_dir = fm.get_checkpoint_path('cp', 'test.ipynb')
68 cp_subdir = fm.get_checkpoint_path('cp', 'test.ipynb', '/%s/' % subd)
68 cp_subdir = fm.get_checkpoint_path('cp', '/%s/test.ipynb' % subd)
69 self.assertNotEqual(cp_dir, cp_subdir)
69 self.assertNotEqual(cp_dir, cp_subdir)
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
70 self.assertEqual(cp_dir, os.path.join(root, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
71 self.assertEqual(cp_subdir, os.path.join(root, subd, fm.checkpoint_dir, cp_name))
@@ -105,11 +105,11 b' class TestContentsManager(TestCase):'
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
108 full_model = cm.get_model(name, path)
108 full_model = cm.get_model(path)
109 nb = full_model['content']
109 nb = full_model['content']
110 self.add_code_cell(nb)
110 self.add_code_cell(nb)
111
111
112 cm.save(full_model, name, path)
112 cm.save(full_model, path)
113 return nb, name, path
113 return nb, name, path
114
114
115 def test_create_file(self):
115 def test_create_file(self):
@@ -120,17 +120,17 b' class TestContentsManager(TestCase):'
120 self.assertIn('name', model)
120 self.assertIn('name', model)
121 self.assertIn('path', model)
121 self.assertIn('path', model)
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
122 self.assertEqual(model['name'], 'Untitled0.ipynb')
123 self.assertEqual(model['path'], '')
123 self.assertEqual(model['path'], 'Untitled0.ipynb')
124
124
125 # Test in sub-directory
125 # Test in sub-directory
126 sub_dir = '/foo/'
126 sub_dir = '/foo/'
127 self.make_dir(cm.root_dir, 'foo')
127 self.make_dir(cm.root_dir, 'foo')
128 model = cm.create_file(None, sub_dir)
128 model = cm.create_file(path=sub_dir)
129 assert isinstance(model, dict)
129 assert isinstance(model, dict)
130 self.assertIn('name', model)
130 self.assertIn('name', model)
131 self.assertIn('path', model)
131 self.assertIn('path', model)
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
132 self.assertEqual(model['name'], 'Untitled0.ipynb')
133 self.assertEqual(model['path'], sub_dir.strip('/'))
133 self.assertEqual(model['path'], 'foo/Untitled0.ipynb')
134
134
135 def test_get(self):
135 def test_get(self):
136 cm = self.contents_manager
136 cm = self.contents_manager
@@ -140,7 +140,7 b' class TestContentsManager(TestCase):'
140 path = model['path']
140 path = model['path']
141
141
142 # Check that we 'get' on the notebook we just created
142 # Check that we 'get' on the notebook we just created
143 model2 = cm.get_model(name, path)
143 model2 = cm.get_model(path)
144 assert isinstance(model2, dict)
144 assert isinstance(model2, dict)
145 self.assertIn('name', model2)
145 self.assertIn('name', model2)
146 self.assertIn('path', model2)
146 self.assertIn('path', model2)
@@ -150,14 +150,14 b' class TestContentsManager(TestCase):'
150 # Test in sub-directory
150 # Test in sub-directory
151 sub_dir = '/foo/'
151 sub_dir = '/foo/'
152 self.make_dir(cm.root_dir, 'foo')
152 self.make_dir(cm.root_dir, 'foo')
153 model = cm.create_file(None, sub_dir)
153 model = cm.create_file(path=sub_dir, ext='.ipynb')
154 model2 = cm.get_model(name, sub_dir)
154 model2 = cm.get_model(sub_dir + name)
155 assert isinstance(model2, dict)
155 assert isinstance(model2, dict)
156 self.assertIn('name', model2)
156 self.assertIn('name', model2)
157 self.assertIn('path', model2)
157 self.assertIn('path', model2)
158 self.assertIn('content', model2)
158 self.assertIn('content', model2)
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
159 self.assertEqual(model2['name'], 'Untitled0.ipynb')
160 self.assertEqual(model2['path'], sub_dir.strip('/'))
160 self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name))
161
161
162 @dec.skip_win32
162 @dec.skip_win32
163 def test_bad_symlink(self):
163 def test_bad_symlink(self):
@@ -175,16 +175,18 b' class TestContentsManager(TestCase):'
175 @dec.skip_win32
175 @dec.skip_win32
176 def test_good_symlink(self):
176 def test_good_symlink(self):
177 cm = self.contents_manager
177 cm = self.contents_manager
178 path = 'test good symlink'
178 parent = 'test good symlink'
179 os_path = self.make_dir(cm.root_dir, path)
179 name = 'good symlink'
180 path = '{0}/{1}'.format(parent, name)
181 os_path = self.make_dir(cm.root_dir, parent)
180
182
181 file_model = cm.create_file(path=path, ext='.txt')
183 file_model = cm.create_file(path=parent, ext='.txt')
182
184
183 # create a good symlink
185 # create a good symlink
184 os.symlink(file_model['name'], os.path.join(os_path, "good symlink"))
186 os.symlink(file_model['name'], os.path.join(os_path, name))
185 symlink_model = cm.get_model(name="good symlink", path=path, content=False)
187 symlink_model = cm.get_model(path, content=False)
186
188
187 dir_model = cm.get_model(path)
189 dir_model = cm.get_model(parent)
188 self.assertEqual(
190 self.assertEqual(
189 sorted(dir_model['content'], key=lambda x: x['name']),
191 sorted(dir_model['content'], key=lambda x: x['name']),
190 [symlink_model, file_model],
192 [symlink_model, file_model],
@@ -198,15 +200,15 b' class TestContentsManager(TestCase):'
198 path = model['path']
200 path = model['path']
199
201
200 # Change the name in the model for rename
202 # Change the name in the model for rename
201 model['name'] = 'test.ipynb'
203 model['path'] = 'test.ipynb'
202 model = cm.update(model, name, path)
204 model = cm.update(model, path)
203 assert isinstance(model, dict)
205 assert isinstance(model, dict)
204 self.assertIn('name', model)
206 self.assertIn('name', model)
205 self.assertIn('path', model)
207 self.assertIn('path', model)
206 self.assertEqual(model['name'], 'test.ipynb')
208 self.assertEqual(model['name'], 'test.ipynb')
207
209
208 # Make sure the old name is gone
210 # Make sure the old name is gone
209 self.assertRaises(HTTPError, cm.get_model, name, path)
211 self.assertRaises(HTTPError, cm.get_model, path)
210
212
211 # Test in sub-directory
213 # Test in sub-directory
212 # Create a directory and notebook in that directory
214 # Create a directory and notebook in that directory
@@ -217,16 +219,17 b' class TestContentsManager(TestCase):'
217 path = model['path']
219 path = model['path']
218
220
219 # Change the name in the model for rename
221 # Change the name in the model for rename
220 model['name'] = 'test_in_sub.ipynb'
222 d = path.rsplit('/', 1)[0]
221 model = cm.update(model, name, path)
223 new_path = model['path'] = d + '/test_in_sub.ipynb'
224 model = cm.update(model, path)
222 assert isinstance(model, dict)
225 assert isinstance(model, dict)
223 self.assertIn('name', model)
226 self.assertIn('name', model)
224 self.assertIn('path', model)
227 self.assertIn('path', model)
225 self.assertEqual(model['name'], 'test_in_sub.ipynb')
228 self.assertEqual(model['name'], 'test_in_sub.ipynb')
226 self.assertEqual(model['path'], sub_dir.strip('/'))
229 self.assertEqual(model['path'], new_path)
227
230
228 # Make sure the old name is gone
231 # Make sure the old name is gone
229 self.assertRaises(HTTPError, cm.get_model, name, path)
232 self.assertRaises(HTTPError, cm.get_model, path)
230
233
231 def test_save(self):
234 def test_save(self):
232 cm = self.contents_manager
235 cm = self.contents_manager
@@ -236,10 +239,10 b' class TestContentsManager(TestCase):'
236 path = model['path']
239 path = model['path']
237
240
238 # Get the model with 'content'
241 # Get the model with 'content'
239 full_model = cm.get_model(name, path)
242 full_model = cm.get_model(path)
240
243
241 # Save the notebook
244 # Save the notebook
242 model = cm.save(full_model, name, path)
245 model = cm.save(full_model, path)
243 assert isinstance(model, dict)
246 assert isinstance(model, dict)
244 self.assertIn('name', model)
247 self.assertIn('name', model)
245 self.assertIn('path', model)
248 self.assertIn('path', model)
@@ -253,15 +256,15 b' class TestContentsManager(TestCase):'
253 model = cm.create_file(None, sub_dir)
256 model = cm.create_file(None, sub_dir)
254 name = model['name']
257 name = model['name']
255 path = model['path']
258 path = model['path']
256 model = cm.get_model(name, path)
259 model = cm.get_model(path)
257
260
258 # Change the name in the model for rename
261 # Change the name in the model for rename
259 model = cm.save(model, name, path)
262 model = cm.save(model, path)
260 assert isinstance(model, dict)
263 assert isinstance(model, dict)
261 self.assertIn('name', model)
264 self.assertIn('name', model)
262 self.assertIn('path', model)
265 self.assertIn('path', model)
263 self.assertEqual(model['name'], 'Untitled0.ipynb')
266 self.assertEqual(model['name'], 'Untitled0.ipynb')
264 self.assertEqual(model['path'], sub_dir.strip('/'))
267 self.assertEqual(model['path'], 'foo/Untitled0.ipynb')
265
268
266 def test_delete(self):
269 def test_delete(self):
267 cm = self.contents_manager
270 cm = self.contents_manager
@@ -269,36 +272,38 b' class TestContentsManager(TestCase):'
269 nb, name, path = self.new_notebook()
272 nb, name, path = self.new_notebook()
270
273
271 # Delete the notebook
274 # Delete the notebook
272 cm.delete(name, path)
275 cm.delete(path)
273
276
274 # Check that a 'get' on the deleted notebook raises and error
277 # Check that a 'get' on the deleted notebook raises and error
275 self.assertRaises(HTTPError, cm.get_model, name, path)
278 self.assertRaises(HTTPError, cm.get_model, path)
276
279
277 def test_copy(self):
280 def test_copy(self):
278 cm = self.contents_manager
281 cm = self.contents_manager
279 path = u'Γ₯ b'
282 parent = u'Γ₯ b'
280 name = u'nb √.ipynb'
283 name = u'nb √.ipynb'
281 os.mkdir(os.path.join(cm.root_dir, path))
284 path = u'{0}/{1}'.format(parent, name)
282 orig = cm.create_file({'name' : name}, path=path)
285 os.mkdir(os.path.join(cm.root_dir, parent))
286 orig = cm.create_file(path=path)
283
287
284 # copy with unspecified name
288 # copy with unspecified name
285 copy = cm.copy(name, path=path)
289 copy = cm.copy(path)
286 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
290 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
287
291
288 # copy with specified name
292 # copy with specified name
289 copy2 = cm.copy(name, u'copy 2.ipynb', path=path)
293 copy2 = cm.copy(path, u'Γ₯ b/copy 2.ipynb')
290 self.assertEqual(copy2['name'], u'copy 2.ipynb')
294 self.assertEqual(copy2['name'], u'copy 2.ipynb')
295 self.assertEqual(copy2['path'], u'Γ₯ b/copy 2.ipynb')
291
296
292 def test_trust_notebook(self):
297 def test_trust_notebook(self):
293 cm = self.contents_manager
298 cm = self.contents_manager
294 nb, name, path = self.new_notebook()
299 nb, name, path = self.new_notebook()
295
300
296 untrusted = cm.get_model(name, path)['content']
301 untrusted = cm.get_model(path)['content']
297 assert not cm.notary.check_cells(untrusted)
302 assert not cm.notary.check_cells(untrusted)
298
303
299 # print(untrusted)
304 # print(untrusted)
300 cm.trust_notebook(name, path)
305 cm.trust_notebook(path)
301 trusted = cm.get_model(name, path)['content']
306 trusted = cm.get_model(path)['content']
302 # print(trusted)
307 # print(trusted)
303 assert cm.notary.check_cells(trusted)
308 assert cm.notary.check_cells(trusted)
304
309
@@ -306,13 +311,13 b' class TestContentsManager(TestCase):'
306 cm = self.contents_manager
311 cm = self.contents_manager
307 nb, name, path = self.new_notebook()
312 nb, name, path = self.new_notebook()
308
313
309 cm.mark_trusted_cells(nb, name, path)
314 cm.mark_trusted_cells(nb, path)
310 for cell in nb.cells:
315 for cell in nb.cells:
311 if cell.cell_type == 'code':
316 if cell.cell_type == 'code':
312 assert not cell.metadata.trusted
317 assert not cell.metadata.trusted
313
318
314 cm.trust_notebook(name, path)
319 cm.trust_notebook(path)
315 nb = cm.get_model(name, path)['content']
320 nb = cm.get_model(path)['content']
316 for cell in nb.cells:
321 for cell in nb.cells:
317 if cell.cell_type == 'code':
322 if cell.cell_type == 'code':
318 assert cell.metadata.trusted
323 assert cell.metadata.trusted
@@ -321,12 +326,12 b' class TestContentsManager(TestCase):'
321 cm = self.contents_manager
326 cm = self.contents_manager
322 nb, name, path = self.new_notebook()
327 nb, name, path = self.new_notebook()
323
328
324 cm.mark_trusted_cells(nb, name, path)
329 cm.mark_trusted_cells(nb, path)
325 cm.check_and_sign(nb, name, path)
330 cm.check_and_sign(nb, path)
326 assert not cm.notary.check_signature(nb)
331 assert not cm.notary.check_signature(nb)
327
332
328 cm.trust_notebook(name, path)
333 cm.trust_notebook(path)
329 nb = cm.get_model(name, path)['content']
334 nb = cm.get_model(path)['content']
330 cm.mark_trusted_cells(nb, name, path)
335 cm.mark_trusted_cells(nb, path)
331 cm.check_and_sign(nb, name, path)
336 cm.check_and_sign(nb, path)
332 assert cm.notary.check_signature(nb)
337 assert cm.notary.check_signature(nb)
@@ -75,22 +75,22 b' class TestInstallNBExtension(TestCase):'
75 td.cleanup()
75 td.cleanup()
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
76 nbextensions.get_ipython_dir = self.save_get_ipython_dir
77
77
78 def assert_path_exists(self, path):
78 def assert_dir_exists(self, path):
79 if not os.path.exists(path):
79 if not os.path.exists(path):
80 do_exist = os.listdir(os.path.dirname(path))
80 do_exist = os.listdir(os.path.dirname(path))
81 self.fail(u"%s should exist (found %s)" % (path, do_exist))
81 self.fail(u"%s should exist (found %s)" % (path, do_exist))
82
82
83 def assert_not_path_exists(self, path):
83 def assert_not_dir_exists(self, path):
84 if os.path.exists(path):
84 if os.path.exists(path):
85 self.fail(u"%s should not exist" % path)
85 self.fail(u"%s should not exist" % path)
86
86
87 def assert_installed(self, relative_path, ipdir=None):
87 def assert_installed(self, relative_path, ipdir=None):
88 self.assert_path_exists(
88 self.assert_dir_exists(
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
89 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
90 )
90 )
91
91
92 def assert_not_installed(self, relative_path, ipdir=None):
92 def assert_not_installed(self, relative_path, ipdir=None):
93 self.assert_not_path_exists(
93 self.assert_not_dir_exists(
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
94 pjoin(ipdir or self.ipdir, u'nbextensions', relative_path)
95 )
95 )
96
96
@@ -99,7 +99,7 b' class TestInstallNBExtension(TestCase):'
99 with TemporaryDirectory() as td:
99 with TemporaryDirectory() as td:
100 ipdir = pjoin(td, u'ipython')
100 ipdir = pjoin(td, u'ipython')
101 install_nbextension(self.src, ipython_dir=ipdir)
101 install_nbextension(self.src, ipython_dir=ipdir)
102 self.assert_path_exists(ipdir)
102 self.assert_dir_exists(ipdir)
103 for file in self.files:
103 for file in self.files:
104 self.assert_installed(
104 self.assert_installed(
105 pjoin(basename(self.src), file),
105 pjoin(basename(self.src), file),
@@ -4,7 +4,7 b''
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from tornado import web
6 from tornado import web
7 from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
7 from ..base.handlers import IPythonHandler, path_regex
8 from ..utils import url_path_join, url_escape
8 from ..utils import url_path_join, url_escape
9
9
10
10
@@ -33,18 +33,18 b' class TreeHandler(IPythonHandler):'
33 return 'Home'
33 return 'Home'
34
34
35 @web.authenticated
35 @web.authenticated
36 def get(self, path='', name=None):
36 def get(self, path=''):
37 path = path.strip('/')
37 path = path.strip('/')
38 cm = self.contents_manager
38 cm = self.contents_manager
39 if name is not None:
39 if cm.file_exists(path):
40 # is a notebook, redirect to notebook handler
40 # is a notebook, redirect to notebook handler
41 url = url_escape(url_path_join(
41 url = url_escape(url_path_join(
42 self.base_url, 'notebooks', path, name
42 self.base_url, 'notebooks', path,
43 ))
43 ))
44 self.log.debug("Redirecting %s to %s", self.request.path, url)
44 self.log.debug("Redirecting %s to %s", self.request.path, url)
45 self.redirect(url)
45 self.redirect(url)
46 else:
46 else:
47 if not cm.path_exists(path=path):
47 if not cm.dir_exists(path=path):
48 # Directory is hidden or does not exist.
48 # Directory is hidden or does not exist.
49 raise web.HTTPError(404)
49 raise web.HTTPError(404)
50 elif cm.is_hidden(path):
50 elif cm.is_hidden(path):
@@ -66,7 +66,6 b' class TreeHandler(IPythonHandler):'
66
66
67
67
68 default_handlers = [
68 default_handlers = [
69 (r"/tree%s" % notebook_path_regex, TreeHandler),
70 (r"/tree%s" % path_regex, TreeHandler),
69 (r"/tree%s" % path_regex, TreeHandler),
71 (r"/tree", TreeHandler),
70 (r"/tree", TreeHandler),
72 ]
71 ]
General Comments 0
You need to be logged in to leave comments. Login now