From e659af75e6884862c7abc755f6d6905b21e3cc67 2014-11-11 22:20:56 From: Thomas Kluyver Date: 2014-11-11 22:20:56 Subject: [PATCH] Allow specifying format when getting files from contents API --- diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 89ae995..67af55c 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -209,11 +209,15 @@ class FileContentsManager(ContentsManager): return model - def _file_model(self, path, content=True): + def _file_model(self, path, content=True, format=None): """Build a model for a file if content is requested, include the file contents. - UTF-8 text files will be unicode, binary files will be base64-encoded. + + format: + If 'text', the contents will be decoded as UTF-8. + If 'base64', the raw bytes contents will be encoded as base64. + If not specified, try to decode as UTF-8, and fall back to base64 """ model = self._base_model(path) model['type'] = 'file' @@ -224,13 +228,20 @@ class FileContentsManager(ContentsManager): raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path) with io.open(os_path, 'rb') as f: bcontent = f.read() - try: - model['content'] = bcontent.decode('utf8') - except UnicodeError as e: + + if format != 'base64': + try: + model['content'] = bcontent.decode('utf8') + except UnicodeError as e: + if format == 'text': + raise web.HTTPError(400, "%s is not UTF-8 encoded" % path) + else: + model['format'] = 'text' + + if model['content'] is None: model['content'] = base64.encodestring(bcontent).decode('ascii') model['format'] = 'base64' - else: - model['format'] = 'text' + return model @@ -255,7 +266,7 @@ class FileContentsManager(ContentsManager): self.validate_notebook_model(model) return model - def get_model(self, path, content=True, type_=None): + def get_model(self, path, content=True, type_=None, format=None): """ Takes a path for an entity and returns its model Parameters @@ -267,6 +278,9 @@ class FileContentsManager(ContentsManager): type_ : str, optional The requested type - 'file', 'notebook', or 'directory'. Will raise HTTPError 406 if the content doesn't match. + format : str, optional + The requested format for file contents. 'text' or 'base64'. + Ignored if this returns a notebook or directory model. Returns ------- @@ -291,7 +305,7 @@ class FileContentsManager(ContentsManager): if type_ == 'directory': raise web.HTTPError(400, u'%s is not a directory') - model = self._file_model(path, content=content) + model = self._file_model(path, content=content, format=format) return model def _save_notebook(self, os_path, model, path=''): diff --git a/IPython/html/services/contents/handlers.py b/IPython/html/services/contents/handlers.py index d097596..8c3526b 100644 --- a/IPython/html/services/contents/handlers.py +++ b/IPython/html/services/contents/handlers.py @@ -62,7 +62,11 @@ class ContentsHandler(IPythonHandler): if type_ not in {None, 'directory', 'file', 'notebook'}: raise web.HTTPError(400, u'Type %r is invalid' % type_) - model = self.contents_manager.get_model(path=path, type_=type_) + format = self.get_query_argument('format', default=None)# + if format not in {None, 'text', 'base64'}: + raise web.HTTPError(400, u'Format %r is invalid' % format) + + model = self.contents_manager.get_model(path=path, type_=type_, format=format) if model['type'] == 'directory': # group listing by type, then by name (case-insensitive) # FIXME: sorting should be done in the frontends diff --git a/IPython/html/services/contents/manager.py b/IPython/html/services/contents/manager.py index ea1a080..d97264a 100644 --- a/IPython/html/services/contents/manager.py +++ b/IPython/html/services/contents/manager.py @@ -135,7 +135,7 @@ class ContentsManager(LoggingConfigurable): """ return self.file_exists(path) or self.dir_exists(path) - def get_model(self, path, content=True, type_=None): + def get_model(self, path, content=True, type_=None, format=None): """Get the model of a file or directory with or without content.""" raise NotImplementedError('must be implemented in a subclass') diff --git a/IPython/html/services/contents/tests/test_contents_api.py b/IPython/html/services/contents/tests/test_contents_api.py index 8c6ce15..5c5b3b9 100644 --- a/IPython/html/services/contents/tests/test_contents_api.py +++ b/IPython/html/services/contents/tests/test_contents_api.py @@ -46,9 +46,14 @@ class API(object): def list(self, path='/'): return self._req('GET', path) - def read(self, path, type_=None): + def read(self, path, type_=None, format=None): + query = [] if type_ is not None: - path += '?type=' + type_ + query.append('type=' + type_) + if format is not None: + query.append('format=' + format) + if query: + path += '?' + '&'.join(query) return self._req('GET', path) def create_untitled(self, path='/', ext='.ipynb'): @@ -245,6 +250,10 @@ class APITest(NotebookTestBase): with assert_http_error(404): self.api.read('foo/q.txt') + # Specifying format=text should fail on a non-UTF-8 file + with assert_http_error(400): + self.api.read('foo/bar/baz.blob', type_='file', format='text') + def test_get_binary_file_contents(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.blob') diff --git a/IPython/html/services/contents/tests/test_manager.py b/IPython/html/services/contents/tests/test_manager.py index b0aa933..baec027 100644 --- a/IPython/html/services/contents/tests/test_manager.py +++ b/IPython/html/services/contents/tests/test_manager.py @@ -165,6 +165,9 @@ class TestContentsManager(TestCase): self.assertEqual(nb_as_file['format'], 'text') self.assertNotIsInstance(nb_as_file['content'], dict) + nb_as_bin_file = cm.get_model(path, content=True, type_='file', format='base64') + self.assertEqual(nb_as_bin_file['format'], 'base64') + # Test in sub-directory sub_dir = '/foo/' self.make_dir(cm.root_dir, 'foo')