##// END OF EJS Templates
add support and tests for uploading and saving regular files
MinRK -
Show More
@@ -196,6 +196,7 b' class FileContentsManager(ContentsManager):'
196 contents.append(self.get_model(name=name, path=dir_path, content=False))
196 contents.append(self.get_model(name=name, path=dir_path, content=False))
197
197
198 model['content'] = sorted(contents, key=sort_key)
198 model['content'] = sorted(contents, key=sort_key)
199 model['format'] = 'json'
199
200
200 return model
201 return model
201
202
@@ -273,12 +274,48 b' class FileContentsManager(ContentsManager):'
273 model = self._file_model(name, path, content)
274 model = self._file_model(name, path, content)
274 return model
275 return model
275
276
277 def _save_notebook(self, os_path, model, name='', path=''):
278 # Save the notebook file
279 nb = current.to_notebook_json(model['content'])
280
281 self.check_and_sign(nb, name, path)
282
283 if 'name' in nb['metadata']:
284 nb['metadata']['name'] = u''
285
286 with io.open(os_path, 'w', encoding='utf-8') as f:
287 current.write(nb, f, u'json')
288
289 def _save_file(self, os_path, model, name='', path=''):
290 fmt = model.get('format', None)
291 if fmt not in {'text', 'base64'}:
292 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
293 try:
294 content = model['content']
295 if fmt == 'text':
296 bcontent = content.encode('utf8')
297 else:
298 b64_bytes = content.encode('ascii')
299 bcontent = base64.decodestring(b64_bytes)
300 except Exception as e:
301 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
302 with io.open(os_path, 'wb') as f:
303 f.write(bcontent)
304
305 def _save_directory(self, os_path, model, name='', path=''):
306 if not os.path.exists(os_path):
307 os.mkdir(os_path)
308 elif not os.path.isdir(os_path):
309 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
310
276 def save(self, model, name='', path=''):
311 def save(self, model, name='', path=''):
277 """Save the notebook model and return the model with no content."""
312 """Save the file model and return the model with no content."""
278 path = path.strip('/')
313 path = path.strip('/')
279
314
280 if 'content' not in model:
315 if 'content' not in model:
281 raise web.HTTPError(400, u'No notebook JSON data provided')
316 raise web.HTTPError(400, u'No file content provided')
317 if 'type' not in model:
318 raise web.HTTPError(400, u'No file type provided')
282
319
283 # One checkpoint should always exist
320 # One checkpoint should always exist
284 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
321 if self.file_exists(name, path) and not self.list_checkpoints(name, path):
@@ -290,20 +327,21 b' class FileContentsManager(ContentsManager):'
290 if path != new_path or name != new_name:
327 if path != new_path or name != new_name:
291 self.rename(name, path, new_name, new_path)
328 self.rename(name, path, new_name, new_path)
292
329
293 # Save the notebook file
294 os_path = self._get_os_path(new_name, new_path)
330 os_path = self._get_os_path(new_name, new_path)
295 nb = current.to_notebook_json(model['content'])
331 self.log.debug("Saving %s", os_path)
296
297 self.check_and_sign(nb, new_name, new_path)
298
299 if 'name' in nb['metadata']:
300 nb['metadata']['name'] = u''
301 try:
332 try:
302 self.log.debug("Autosaving notebook %s", os_path)
333 if model['type'] == 'notebook':
303 with io.open(os_path, 'w', encoding='utf-8') as f:
334 self._save_notebook(os_path, model, new_name, new_path)
304 current.write(nb, f, u'json')
335 elif model['type'] == 'file':
336 self._save_file(os_path, model, new_name, new_path)
337 elif model['type'] == 'directory':
338 self._save_directory(os_path, model, new_name, new_path)
339 else:
340 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
341 except web.HTTPError:
342 raise
305 except Exception as e:
343 except Exception as e:
306 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
344 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
307
345
308 model = self.get_model(new_name, new_path, content=False)
346 model = self.get_model(new_name, new_path, content=False)
309 return model
347 return model
@@ -99,20 +99,20 b' class ContentsHandler(IPythonHandler):'
99 if name:
99 if name:
100 model['name'] = name
100 model['name'] = name
101
101
102 model = self.contents_manager.create_notebook(model, path)
102 model = self.contents_manager.create_file(model, path)
103 self.set_status(201)
103 self.set_status(201)
104 self._finish_model(model)
104 self._finish_model(model)
105
105
106 def _create_empty_notebook(self, path, name=None):
106 def _create_empty_file(self, path, name=None, ext='.ipynb'):
107 """Create an empty notebook in path
107 """Create an empty file in path
108
108
109 If name specified, create it in path/name.
109 If name specified, create it in path/name.
110 """
110 """
111 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
111 self.log.info(u"Creating new file in %s/%s", path, name or '')
112 model = {}
112 model = {}
113 if name:
113 if name:
114 model['name'] = name
114 model['name'] = name
115 model = self.contents_manager.create_notebook(model, path=path)
115 model = self.contents_manager.create_file(model, path=path, ext=ext)
116 self.set_status(201)
116 self.set_status(201)
117 self._finish_model(model)
117 self._finish_model(model)
118
118
@@ -137,7 +137,8 b' class ContentsHandler(IPythonHandler):'
137 POST /api/contents/path
137 POST /api/contents/path
138 New untitled notebook in path. If content specified, upload a
138 New untitled notebook in path. If content specified, upload a
139 notebook, otherwise start empty.
139 notebook, otherwise start empty.
140 POST /api/contents/path?copy=OtherNotebook.ipynb
140 POST /api/contents/path
141 with body {"copy_from" : "OtherNotebook.ipynb"}
141 New copy of OtherNotebook in path
142 New copy of OtherNotebook in path
142 """
143 """
143
144
@@ -156,14 +157,17 b' class ContentsHandler(IPythonHandler):'
156
157
157 if model is not None:
158 if model is not None:
158 copy_from = model.get('copy_from')
159 copy_from = model.get('copy_from')
159 if copy_from:
160 ext = model.get('ext', '.ipynb')
160 if model.get('content'):
161 if model.get('content') is not None:
162 if copy_from:
161 raise web.HTTPError(400, "Can't upload and copy at the same time.")
163 raise web.HTTPError(400, "Can't upload and copy at the same time.")
164 self._upload(model, path)
165 elif copy_from:
162 self._copy(copy_from, path)
166 self._copy(copy_from, path)
163 else:
167 else:
164 self._upload(model, path)
168 self._create_empty_file(path, ext=ext)
165 else:
169 else:
166 self._create_empty_notebook(path)
170 self._create_empty_file(path)
167
171
168 @web.authenticated
172 @web.authenticated
169 @json_errors
173 @json_errors
@@ -195,7 +199,7 b' class ContentsHandler(IPythonHandler):'
195 else:
199 else:
196 self._upload(model, path, name)
200 self._upload(model, path, name)
197 else:
201 else:
198 self._create_empty_notebook(path, name)
202 self._create_empty_file(path, name)
199
203
200 @web.authenticated
204 @web.authenticated
201 @json_errors
205 @json_errors
@@ -155,16 +155,23 b' class ContentsManager(LoggingConfigurable):'
155 break
155 break
156 return name
156 return name
157
157
158 def create_notebook(self, model=None, path=''):
158 def create_file(self, model=None, path='', ext='.ipynb'):
159 """Create a new notebook and return its model with no content."""
159 """Create a new notebook and return its model with no content."""
160 path = path.strip('/')
160 path = path.strip('/')
161 if model is None:
161 if model is None:
162 model = {}
162 model = {}
163 if 'content' not in model:
163 if 'content' not in model:
164 metadata = current.new_metadata(name=u'')
164 if ext == '.ipynb':
165 model['content'] = current.new_notebook(metadata=metadata)
165 metadata = current.new_metadata(name=u'')
166 model['content'] = current.new_notebook(metadata=metadata)
167 model.setdefault('type', 'notebook')
168 model.setdefault('format', 'json')
169 else:
170 model['content'] = ''
171 model.setdefault('type', 'file')
172 model.setdefault('format', 'text')
166 if 'name' not in model:
173 if 'name' not in model:
167 model['name'] = self.increment_filename('Untitled.ipynb', path)
174 model['name'] = self.increment_filename('Untitled' + ext, path)
168
175
169 model['path'] = path
176 model['path'] = path
170 model = self.save(model, model['name'], model['path'])
177 model = self.save(model, model['name'], model['path'])
@@ -50,8 +50,11 b' class API(object):'
50 def read(self, name, path='/'):
50 def read(self, name, path='/'):
51 return self._req('GET', url_path_join(path, name))
51 return self._req('GET', url_path_join(path, name))
52
52
53 def create_untitled(self, path='/'):
53 def create_untitled(self, path='/', ext=None):
54 return self._req('POST', path)
54 body = None
55 if ext:
56 body = json.dumps({'ext': ext})
57 return self._req('POST', path, body)
55
58
56 def upload_untitled(self, body, path='/'):
59 def upload_untitled(self, body, path='/'):
57 return self._req('POST', path, body)
60 return self._req('POST', path, body)
@@ -267,26 +270,72 b' class APITest(NotebookTestBase):'
267 resp = self.api.create_untitled(path='foo/bar')
270 resp = self.api.create_untitled(path='foo/bar')
268 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
271 self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar')
269
272
273 def test_create_untitled_txt(self):
274 resp = self.api.create_untitled(path='foo/bar', ext='.txt')
275 self._check_nb_created(resp, 'Untitled0.txt', 'foo/bar')
276
277 resp = self.api.read(path='foo/bar', name='Untitled0.txt')
278 model = resp.json()
279 self.assertEqual(model['type'], 'file')
280 self.assertEqual(model['format'], 'text')
281 self.assertEqual(model['content'], '')
282
270 def test_upload_untitled(self):
283 def test_upload_untitled(self):
271 nb = new_notebook(name='Upload test')
284 nb = new_notebook(name='Upload test')
272 nbmodel = {'content': nb}
285 nbmodel = {'content': nb, 'type': 'notebook'}
273 resp = self.api.upload_untitled(path=u'å b',
286 resp = self.api.upload_untitled(path=u'å b',
274 body=json.dumps(nbmodel))
287 body=json.dumps(nbmodel))
275 self._check_nb_created(resp, 'Untitled0.ipynb', u'å b')
288 self._check_nb_created(resp, 'Untitled0.ipynb', u'å b')
276
289
277 def test_upload(self):
290 def test_upload(self):
278 nb = new_notebook(name=u'ignored')
291 nb = new_notebook(name=u'ignored')
279 nbmodel = {'content': nb}
292 nbmodel = {'content': nb, 'type': 'notebook'}
280 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
293 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
281 body=json.dumps(nbmodel))
294 body=json.dumps(nbmodel))
282 self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
295 self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
283
296
297 def test_upload_txt(self):
298 body = u'ünicode téxt'
299 model = {
300 'content' : body,
301 'format' : 'text',
302 'type' : 'file',
303 }
304 resp = self.api.upload(u'Upload tést.txt', path=u'å b',
305 body=json.dumps(model))
306
307 # check roundtrip
308 resp = self.api.read(path=u'å b', name=u'Upload tést.txt')
309 model = resp.json()
310 self.assertEqual(model['type'], 'file')
311 self.assertEqual(model['format'], 'text')
312 self.assertEqual(model['content'], body)
313
314 def test_upload_b64(self):
315 body = b'\xFFblob'
316 b64body = base64.encodestring(body).decode('ascii')
317 model = {
318 'content' : b64body,
319 'format' : 'base64',
320 'type' : 'file',
321 }
322 resp = self.api.upload(u'Upload tést.blob', path=u'å b',
323 body=json.dumps(model))
324
325 # check roundtrip
326 resp = self.api.read(path=u'å b', name=u'Upload tést.blob')
327 model = resp.json()
328 self.assertEqual(model['type'], 'file')
329 self.assertEqual(model['format'], 'base64')
330 decoded = base64.decodestring(model['content'].encode('ascii'))
331 self.assertEqual(decoded, body)
332
284 def test_upload_v2(self):
333 def test_upload_v2(self):
285 nb = v2.new_notebook()
334 nb = v2.new_notebook()
286 ws = v2.new_worksheet()
335 ws = v2.new_worksheet()
287 nb.worksheets.append(ws)
336 nb.worksheets.append(ws)
288 ws.cells.append(v2.new_code_cell(input='print("hi")'))
337 ws.cells.append(v2.new_code_cell(input='print("hi")'))
289 nbmodel = {'content': nb}
338 nbmodel = {'content': nb, 'type': 'notebook'}
290 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
339 resp = self.api.upload(u'Upload tést.ipynb', path=u'å b',
291 body=json.dumps(nbmodel))
340 body=json.dumps(nbmodel))
292 self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
341 self._check_nb_created(resp, u'Upload tést.ipynb', u'å b')
@@ -335,7 +384,7 b' class APITest(NotebookTestBase):'
335 nb.worksheets = [ws]
384 nb.worksheets = [ws]
336 ws.cells.append(new_heading_cell(u'Created by test ³'))
385 ws.cells.append(new_heading_cell(u'Created by test ³'))
337
386
338 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
387 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
339 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
388 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
340
389
341 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
390 nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb')
@@ -349,7 +398,7 b' class APITest(NotebookTestBase):'
349 u'Created by test ³')
398 u'Created by test ³')
350
399
351 # Save and rename
400 # Save and rename
352 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb}
401 nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'}
353 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
402 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
354 saved = resp.json()
403 saved = resp.json()
355 self.assertEqual(saved['name'], 'a2.ipynb')
404 self.assertEqual(saved['name'], 'a2.ipynb')
@@ -375,7 +424,7 b' class APITest(NotebookTestBase):'
375 hcell = new_heading_cell('Created by test')
424 hcell = new_heading_cell('Created by test')
376 ws.cells.append(hcell)
425 ws.cells.append(hcell)
377 # Save
426 # Save
378 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb}
427 nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'}
379 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
428 resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel))
380
429
381 # List checkpoints
430 # List checkpoints
@@ -101,7 +101,7 b' class TestContentsManager(TestCase):'
101
101
102 def new_notebook(self):
102 def new_notebook(self):
103 cm = self.contents_manager
103 cm = self.contents_manager
104 model = cm.create_notebook()
104 model = cm.create_file()
105 name = model['name']
105 name = model['name']
106 path = model['path']
106 path = model['path']
107
107
@@ -112,10 +112,10 b' class TestContentsManager(TestCase):'
112 cm.save(full_model, name, path)
112 cm.save(full_model, name, path)
113 return nb, name, path
113 return nb, name, path
114
114
115 def test_create_notebook(self):
115 def test_create_file(self):
116 cm = self.contents_manager
116 cm = self.contents_manager
117 # Test in root directory
117 # Test in root directory
118 model = cm.create_notebook()
118 model = cm.create_file()
119 assert isinstance(model, dict)
119 assert isinstance(model, dict)
120 self.assertIn('name', model)
120 self.assertIn('name', model)
121 self.assertIn('path', model)
121 self.assertIn('path', model)
@@ -125,7 +125,7 b' class TestContentsManager(TestCase):'
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_notebook(None, sub_dir)
128 model = cm.create_file(None, 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)
@@ -135,7 +135,7 b' class TestContentsManager(TestCase):'
135 def test_get(self):
135 def test_get(self):
136 cm = self.contents_manager
136 cm = self.contents_manager
137 # Create a notebook
137 # Create a notebook
138 model = cm.create_notebook()
138 model = cm.create_file()
139 name = model['name']
139 name = model['name']
140 path = model['path']
140 path = model['path']
141
141
@@ -150,7 +150,7 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_notebook(None, sub_dir)
153 model = cm.create_file(None, sub_dir)
154 model2 = cm.get_model(name, sub_dir)
154 model2 = cm.get_model(name, sub_dir)
155 assert isinstance(model2, dict)
155 assert isinstance(model2, dict)
156 self.assertIn('name', model2)
156 self.assertIn('name', model2)
@@ -162,7 +162,7 b' class TestContentsManager(TestCase):'
162 def test_update(self):
162 def test_update(self):
163 cm = self.contents_manager
163 cm = self.contents_manager
164 # Create a notebook
164 # Create a notebook
165 model = cm.create_notebook()
165 model = cm.create_file()
166 name = model['name']
166 name = model['name']
167 path = model['path']
167 path = model['path']
168
168
@@ -181,7 +181,7 b' class TestContentsManager(TestCase):'
181 # Create a directory and notebook in that directory
181 # Create a directory and notebook in that directory
182 sub_dir = '/foo/'
182 sub_dir = '/foo/'
183 self.make_dir(cm.root_dir, 'foo')
183 self.make_dir(cm.root_dir, 'foo')
184 model = cm.create_notebook(None, sub_dir)
184 model = cm.create_file(None, sub_dir)
185 name = model['name']
185 name = model['name']
186 path = model['path']
186 path = model['path']
187
187
@@ -200,7 +200,7 b' class TestContentsManager(TestCase):'
200 def test_save(self):
200 def test_save(self):
201 cm = self.contents_manager
201 cm = self.contents_manager
202 # Create a notebook
202 # Create a notebook
203 model = cm.create_notebook()
203 model = cm.create_file()
204 name = model['name']
204 name = model['name']
205 path = model['path']
205 path = model['path']
206
206
@@ -219,7 +219,7 b' class TestContentsManager(TestCase):'
219 # Create a directory and notebook in that directory
219 # Create a directory and notebook in that directory
220 sub_dir = '/foo/'
220 sub_dir = '/foo/'
221 self.make_dir(cm.root_dir, 'foo')
221 self.make_dir(cm.root_dir, 'foo')
222 model = cm.create_notebook(None, sub_dir)
222 model = cm.create_file(None, sub_dir)
223 name = model['name']
223 name = model['name']
224 path = model['path']
224 path = model['path']
225 model = cm.get_model(name, path)
225 model = cm.get_model(name, path)
@@ -248,7 +248,7 b' class TestContentsManager(TestCase):'
248 path = u'å b'
248 path = u'å b'
249 name = u'nb √.ipynb'
249 name = u'nb √.ipynb'
250 os.mkdir(os.path.join(cm.root_dir, path))
250 os.mkdir(os.path.join(cm.root_dir, path))
251 orig = cm.create_notebook({'name' : name}, path=path)
251 orig = cm.create_file({'name' : name}, path=path)
252
252
253 # copy with unspecified name
253 # copy with unspecified name
254 copy = cm.copy(name, path=path)
254 copy = cm.copy(name, path=path)
@@ -1885,6 +1885,8 b' define(['
1885 var model = {};
1885 var model = {};
1886 model.name = this.notebook_name;
1886 model.name = this.notebook_name;
1887 model.path = this.notebook_path;
1887 model.path = this.notebook_path;
1888 model.type = 'notebook';
1889 model.format = 'json';
1888 model.content = this.toJSON();
1890 model.content = this.toJSON();
1889 model.content.nbformat = this.nbformat;
1891 model.content.nbformat = this.nbformat;
1890 model.content.nbformat_minor = this.nbformat_minor;
1892 model.content.nbformat_minor = this.nbformat_minor;
General Comments 0
You need to be logged in to leave comments. Login now