From f8d34e13b8013c868b3003dc560672c8628146b8 2014-07-31 18:57:36 From: MinRK Date: 2014-07-31 18:57:36 Subject: [PATCH] add support and tests for uploading and saving regular files --- diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index e4a0b59..e9fab0b 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -196,6 +196,7 @@ class FileContentsManager(ContentsManager): contents.append(self.get_model(name=name, path=dir_path, content=False)) model['content'] = sorted(contents, key=sort_key) + model['format'] = 'json' return model @@ -273,12 +274,48 @@ class FileContentsManager(ContentsManager): model = self._file_model(name, path, content) return model + def _save_notebook(self, os_path, model, name='', path=''): + # Save the notebook file + nb = current.to_notebook_json(model['content']) + + self.check_and_sign(nb, name, path) + + if 'name' in nb['metadata']: + nb['metadata']['name'] = u'' + + with io.open(os_path, 'w', encoding='utf-8') as f: + current.write(nb, f, u'json') + + def _save_file(self, os_path, model, name='', path=''): + fmt = model.get('format', None) + if fmt not in {'text', 'base64'}: + raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'") + try: + content = model['content'] + if fmt == 'text': + bcontent = content.encode('utf8') + else: + b64_bytes = content.encode('ascii') + bcontent = base64.decodestring(b64_bytes) + except Exception as e: + raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e)) + with io.open(os_path, 'wb') as f: + f.write(bcontent) + + def _save_directory(self, os_path, model, name='', path=''): + if not os.path.exists(os_path): + os.mkdir(os_path) + elif not os.path.isdir(os_path): + raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) + def save(self, model, name='', path=''): - """Save the notebook model and return the model with no content.""" + """Save the file model and return the model with no content.""" path = path.strip('/') if 'content' not in model: - raise web.HTTPError(400, u'No notebook JSON data provided') + raise web.HTTPError(400, u'No file content provided') + if 'type' not in model: + raise web.HTTPError(400, u'No file type provided') # One checkpoint should always exist if self.file_exists(name, path) and not self.list_checkpoints(name, path): @@ -290,20 +327,21 @@ class FileContentsManager(ContentsManager): if path != new_path or name != new_name: self.rename(name, path, new_name, new_path) - # Save the notebook file os_path = self._get_os_path(new_name, new_path) - nb = current.to_notebook_json(model['content']) - - self.check_and_sign(nb, new_name, new_path) - - if 'name' in nb['metadata']: - nb['metadata']['name'] = u'' + self.log.debug("Saving %s", os_path) try: - self.log.debug("Autosaving notebook %s", os_path) - with io.open(os_path, 'w', encoding='utf-8') as f: - current.write(nb, f, u'json') + if model['type'] == 'notebook': + self._save_notebook(os_path, model, new_name, new_path) + elif model['type'] == 'file': + self._save_file(os_path, model, new_name, new_path) + elif model['type'] == 'directory': + self._save_directory(os_path, model, new_name, new_path) + else: + raise web.HTTPError(400, "Unhandled contents type: %s" % model['type']) + except web.HTTPError: + raise except Exception as e: - raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e)) + raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e)) model = self.get_model(new_name, new_path, content=False) return model diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index e649525..e6f08ed 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -99,20 +99,20 @@ class ContentsHandler(IPythonHandler): if name: model['name'] = name - model = self.contents_manager.create_notebook(model, path) + model = self.contents_manager.create_file(model, path) self.set_status(201) self._finish_model(model) - def _create_empty_notebook(self, path, name=None): - """Create an empty notebook in path + def _create_empty_file(self, path, name=None, ext='.ipynb'): + """Create an empty file in path If name specified, create it in path/name. """ - self.log.info(u"Creating new notebook in %s/%s", path, name or '') + self.log.info(u"Creating new file in %s/%s", path, name or '') model = {} if name: model['name'] = name - model = self.contents_manager.create_notebook(model, path=path) + model = self.contents_manager.create_file(model, path=path, ext=ext) self.set_status(201) self._finish_model(model) @@ -137,7 +137,8 @@ class ContentsHandler(IPythonHandler): POST /api/contents/path New untitled notebook in path. If content specified, upload a notebook, otherwise start empty. - POST /api/contents/path?copy=OtherNotebook.ipynb + POST /api/contents/path + with body {"copy_from" : "OtherNotebook.ipynb"} New copy of OtherNotebook in path """ @@ -156,14 +157,17 @@ class ContentsHandler(IPythonHandler): if model is not None: copy_from = model.get('copy_from') - if copy_from: - if model.get('content'): + ext = model.get('ext', '.ipynb') + if model.get('content') is not None: + if copy_from: raise web.HTTPError(400, "Can't upload and copy at the same time.") + self._upload(model, path) + elif copy_from: self._copy(copy_from, path) else: - self._upload(model, path) + self._create_empty_file(path, ext=ext) else: - self._create_empty_notebook(path) + self._create_empty_file(path) @web.authenticated @json_errors @@ -195,7 +199,7 @@ class ContentsHandler(IPythonHandler): else: self._upload(model, path, name) else: - self._create_empty_notebook(path, name) + self._create_empty_file(path, name) @web.authenticated @json_errors diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index dff77b5..8cec398 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -155,16 +155,23 @@ class ContentsManager(LoggingConfigurable): break return name - def create_notebook(self, model=None, path=''): + def create_file(self, model=None, path='', ext='.ipynb'): """Create a new notebook and return its model with no content.""" path = path.strip('/') if model is None: model = {} if 'content' not in model: - metadata = current.new_metadata(name=u'') - model['content'] = current.new_notebook(metadata=metadata) + if ext == '.ipynb': + metadata = current.new_metadata(name=u'') + model['content'] = current.new_notebook(metadata=metadata) + model.setdefault('type', 'notebook') + model.setdefault('format', 'json') + else: + model['content'] = '' + model.setdefault('type', 'file') + model.setdefault('format', 'text') if 'name' not in model: - model['name'] = self.increment_filename('Untitled.ipynb', path) + model['name'] = self.increment_filename('Untitled' + ext, path) model['path'] = path model = self.save(model, model['name'], model['path']) diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index 5381b82..d0e3e20 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -50,8 +50,11 @@ class API(object): def read(self, name, path='/'): return self._req('GET', url_path_join(path, name)) - def create_untitled(self, path='/'): - return self._req('POST', path) + def create_untitled(self, path='/', ext=None): + body = None + if ext: + body = json.dumps({'ext': ext}) + return self._req('POST', path, body) def upload_untitled(self, body, path='/'): return self._req('POST', path, body) @@ -267,26 +270,72 @@ class APITest(NotebookTestBase): resp = self.api.create_untitled(path='foo/bar') self._check_nb_created(resp, 'Untitled0.ipynb', 'foo/bar') + def test_create_untitled_txt(self): + resp = self.api.create_untitled(path='foo/bar', ext='.txt') + self._check_nb_created(resp, 'Untitled0.txt', 'foo/bar') + + resp = self.api.read(path='foo/bar', name='Untitled0.txt') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'text') + self.assertEqual(model['content'], '') + def test_upload_untitled(self): nb = new_notebook(name='Upload test') - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload_untitled(path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, 'Untitled0.ipynb', u'å b') def test_upload(self): nb = new_notebook(name=u'ignored') - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') + def test_upload_txt(self): + body = u'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + } + resp = self.api.upload(u'Upload tést.txt', path=u'å b', + body=json.dumps(model)) + + # check roundtrip + resp = self.api.read(path=u'å b', name=u'Upload tést.txt') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'text') + self.assertEqual(model['content'], body) + + def test_upload_b64(self): + body = b'\xFFblob' + b64body = base64.encodestring(body).decode('ascii') + model = { + 'content' : b64body, + 'format' : 'base64', + 'type' : 'file', + } + resp = self.api.upload(u'Upload tést.blob', path=u'å b', + body=json.dumps(model)) + + # check roundtrip + resp = self.api.read(path=u'å b', name=u'Upload tést.blob') + model = resp.json() + self.assertEqual(model['type'], 'file') + self.assertEqual(model['format'], 'base64') + decoded = base64.decodestring(model['content'].encode('ascii')) + self.assertEqual(decoded, body) + def test_upload_v2(self): nb = v2.new_notebook() ws = v2.new_worksheet() nb.worksheets.append(ws) ws.cells.append(v2.new_code_cell(input='print("hi")')) - nbmodel = {'content': nb} + nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.upload(u'Upload tést.ipynb', path=u'å b', body=json.dumps(nbmodel)) self._check_nb_created(resp, u'Upload tést.ipynb', u'å b') @@ -335,7 +384,7 @@ class APITest(NotebookTestBase): nb.worksheets = [ws] ws.cells.append(new_heading_cell(u'Created by test ³')) - nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} + nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) nbfile = pjoin(self.notebook_dir.name, 'foo', 'a.ipynb') @@ -349,7 +398,7 @@ class APITest(NotebookTestBase): u'Created by test ³') # Save and rename - nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb} + nbmodel= {'name': 'a2.ipynb', 'path':'foo/bar', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) saved = resp.json() self.assertEqual(saved['name'], 'a2.ipynb') @@ -375,7 +424,7 @@ class APITest(NotebookTestBase): hcell = new_heading_cell('Created by test') ws.cells.append(hcell) # Save - nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb} + nbmodel= {'name': 'a.ipynb', 'path':'foo', 'content': nb, 'type': 'notebook'} resp = self.api.save('a.ipynb', path='foo', body=json.dumps(nbmodel)) # List checkpoints diff --git a/IPython/html/services/contents/tests/test_manager.py b/IPython/html/services/contents/tests/test_manager.py index 44f7e33..e58a895 100644 --- a/IPython/html/services/contents/tests/test_manager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -101,7 +101,7 @@ class TestContentsManager(TestCase): def new_notebook(self): cm = self.contents_manager - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -112,10 +112,10 @@ class TestContentsManager(TestCase): cm.save(full_model, name, path) return nb, name, path - def test_create_notebook(self): + def test_create_file(self): cm = self.contents_manager # Test in root directory - model = cm.create_notebook() + model = cm.create_file() assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -125,7 +125,7 @@ class TestContentsManager(TestCase): # Test in sub-directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) assert isinstance(model, dict) self.assertIn('name', model) self.assertIn('path', model) @@ -135,7 +135,7 @@ class TestContentsManager(TestCase): def test_get(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -150,7 +150,7 @@ class TestContentsManager(TestCase): # Test in sub-directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) model2 = cm.get_model(name, sub_dir) assert isinstance(model2, dict) self.assertIn('name', model2) @@ -162,7 +162,7 @@ class TestContentsManager(TestCase): def test_update(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -181,7 +181,7 @@ class TestContentsManager(TestCase): # Create a directory and notebook in that directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) name = model['name'] path = model['path'] @@ -200,7 +200,7 @@ class TestContentsManager(TestCase): def test_save(self): cm = self.contents_manager # Create a notebook - model = cm.create_notebook() + model = cm.create_file() name = model['name'] path = model['path'] @@ -219,7 +219,7 @@ class TestContentsManager(TestCase): # Create a directory and notebook in that directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo') - model = cm.create_notebook(None, sub_dir) + model = cm.create_file(None, sub_dir) name = model['name'] path = model['path'] model = cm.get_model(name, path) @@ -248,7 +248,7 @@ class TestContentsManager(TestCase): path = u'å b' name = u'nb √.ipynb' os.mkdir(os.path.join(cm.root_dir, path)) - orig = cm.create_notebook({'name' : name}, path=path) + orig = cm.create_file({'name' : name}, path=path) # copy with unspecified name copy = cm.copy(name, path=path) diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index 67e5e8b..02a4439 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -1885,6 +1885,8 @@ define([ var model = {}; model.name = this.notebook_name; model.path = this.notebook_path; + model.type = 'notebook'; + model.format = 'json'; model.content = this.toJSON(); model.content.nbformat = this.nbformat; model.content.nbformat_minor = this.nbformat_minor;