浏览代码

HUE-8253 [editor] Support downloading Query results with query names(file names) other than ISO-8859-1 charset

Roohi 7 年之前
父节点
当前提交
bd2beaf7bf

+ 2 - 2
apps/beeswax/src/beeswax/data_export.py

@@ -33,7 +33,7 @@ FETCH_SIZE = 1000
 DOWNLOAD_COOKIE_AGE = 1800 # 30 minutes
 DOWNLOAD_COOKIE_AGE = 1800 # 30 minutes
 
 
 
 
-def download(handle, format, db, id=None, file_name='query_result'):
+def download(handle, format, db, id=None, file_name='query_result', user_agent=None):
   """
   """
   download(query_model, format) -> HttpResponse
   download(query_model, format) -> HttpResponse
 
 
@@ -49,7 +49,7 @@ def download(handle, format, db, id=None, file_name='query_result'):
   content_generator = HS2DataAdapter(handle, db, max_rows=max_rows, start_over=True, max_bytes=max_bytes)
   content_generator = HS2DataAdapter(handle, db, max_rows=max_rows, start_over=True, max_bytes=max_bytes)
   generator = export_csvxls.create_generator(content_generator, format)
   generator = export_csvxls.create_generator(content_generator, format)
 
 
-  resp = export_csvxls.make_response(generator, format, file_name)
+  resp = export_csvxls.make_response(generator, format, file_name, user_agent=user_agent)
 
 
   if id:
   if id:
     resp.set_cookie(
     resp.set_cookie(

+ 2 - 2
apps/beeswax/src/beeswax/views.py

@@ -369,7 +369,7 @@ def massage_query_history_for_json(app_name, query_history):
   }
   }
 
 
 
 
-def download(request, id, format):
+def download(request, id, format, user_agent=None):
   if not ENABLE_DOWNLOAD.get():
   if not ENABLE_DOWNLOAD.get():
     return serve_403_error(request)
     return serve_403_error(request)
 
 
@@ -378,7 +378,7 @@ def download(request, id, format):
     db = dbms.get(request.user, query_history.get_query_server_config())
     db = dbms.get(request.user, query_history.get_query_server_config())
     LOG.debug('Download results for query %s: [ %s ]' % (query_history.server_id, query_history.query))
     LOG.debug('Download results for query %s: [ %s ]' % (query_history.server_id, query_history.query))
 
 
-    return data_export.download(query_history.get_handle(), format, db)
+    return data_export.download(query_history.get_handle(), format, db, user_agent=user_agent)
   except Exception, e:
   except Exception, e:
     if not hasattr(e, 'message') or not e.message:
     if not hasattr(e, 'message') or not e.message:
       message = e
       message = e

+ 12 - 2
desktop/core/src/desktop/lib/export_csvxls.py

@@ -29,6 +29,7 @@ import tablib
 
 
 from django.http import StreamingHttpResponse, HttpResponse
 from django.http import StreamingHttpResponse, HttpResponse
 from django.utils.encoding import smart_str
 from django.utils.encoding import smart_str
+from django.utils.http import urlquote
 from desktop.lib import i18n
 from desktop.lib import i18n
 
 
 
 
@@ -112,7 +113,7 @@ def create_generator(content_generator, format, encoding=None):
     raise Exception("Unknown format: %s" % format)
     raise Exception("Unknown format: %s" % format)
 
 
 
 
-def make_response(generator, format, name, encoding=None):
+def make_response(generator, format, name, encoding=None, user_agent=None):
   """
   """
   @param data An iterator of rows, where every row is a list of strings
   @param data An iterator of rows, where every row is a list of strings
   @param format Either "csv" or "xls"
   @param format Either "csv" or "xls"
@@ -137,6 +138,15 @@ def make_response(generator, format, name, encoding=None):
   else:
   else:
     raise Exception("Unknown format: %s" % format)
     raise Exception("Unknown format: %s" % format)
 
 
-  resp['Content-Disposition'] = 'attachment; filename="%s.%s"' % (name, format)
+  try:
+    name = name.encode('ascii')
+    resp['Content-Disposition'] = 'attachment; filename="%s.%s"' % (name, format)
+  except UnicodeEncodeError:
+    name = urlquote(name)
+    if user_agent is not None and 'Firefox' in user_agent:
+      # Preserving non-ASCII filename. See RFC https://tools.ietf.org/html/rfc6266#appendix-D, only FF works
+      resp['Content-Disposition'] = 'attachment; filename*="%s.%s"' % (name, format)
+    else:
+      resp['Content-Disposition'] = 'attachment; filename="%s.%s"' % (name, format)
 
 
   return resp
   return resp

+ 20 - 0
desktop/core/src/desktop/lib/test_export_csvxls.py

@@ -1,4 +1,5 @@
 #!/usr/bin/env python
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
 # Licensed to Cloudera, Inc. under one
 # Licensed to Cloudera, Inc. under one
 # or more contributor license agreements.  See the NOTICE file
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
 # distributed with this work for additional information
@@ -39,6 +40,25 @@ def test_export_csv():
   assert_equal('x,y\r\n1,2\r\n3,4\r\n"5,6",7\r\nNULL,NULL\r\nhttp://gethue.com,http://gethue.com\r\n', content)
   assert_equal('x,y\r\n1,2\r\n3,4\r\n"5,6",7\r\nNULL,NULL\r\nhttp://gethue.com,http://gethue.com\r\n', content)
   assert_equal('attachment; filename="foo.csv"', response["content-disposition"])
   assert_equal('attachment; filename="foo.csv"', response["content-disposition"])
 
 
+  # Check non-ASCII for any browser except FF or no browser info
+  generator = create_generator(content_generator(headers, data), "csv")
+  response = make_response(generator, "csv", u'gんtbhんjk?¥n')
+  assert_equal("application/csv", response["content-type"])
+  content = ''.join(response.streaming_content)
+  assert_equal('x,y\r\n1,2\r\n3,4\r\n"5,6",7\r\nNULL,NULL\r\nhttp://gethue.com,http://gethue.com\r\n', content)
+  assert_equal('attachment; filename="g%E3%82%93tbh%E3%82%93jk%EF%BC%9F%EF%BF%A5n.csv"', response["content-disposition"])
+
+  # Check non-ASCII for FF browser
+  generator = create_generator(content_generator(headers, data), "csv")
+  response = make_response(generator, "csv", u'gんtbhんjk?¥n',
+                           user_agent='Mozilla / 5.0(Macintosh; Intel Mac OS X 10.12;rv:59.0) Gecko / 20100101 Firefox / 59.0)')
+  assert_equal("application/csv", response["content-type"])
+  content = ''.join(response.streaming_content)
+  assert_equal('x,y\r\n1,2\r\n3,4\r\n"5,6",7\r\nNULL,NULL\r\nhttp://gethue.com,http://gethue.com\r\n', content)
+  assert_equal('attachment; filename*="g%E3%82%93tbh%E3%82%93jk%EF%BC%9F%EF%BF%A5n.csv"',
+               response["content-disposition"])
+
+
 
 
 def test_export_xls():
 def test_export_xls():
   headers = ["x", "y"]
   headers = ["x", "y"]

+ 1 - 1
desktop/libs/dashboard/src/dashboard/api.py

@@ -299,7 +299,7 @@ def download(request):
       resp['Content-Disposition'] = 'attachment; filename="%s.%s"' % ('query_result', file_format)
       resp['Content-Disposition'] = 'attachment; filename="%s.%s"' % ('query_result', file_format)
       return resp
       return resp
     else:
     else:
-      return export_download(response, file_format, collection)
+      return export_download(response, file_format, collection, user_agent=request.META.get('HTTP_USER_AGENT'))
   except Exception, e:
   except Exception, e:
     raise PopupException(_("Could not download search results: %s") % e)
     raise PopupException(_("Could not download search results: %s") % e)
 
 

+ 2 - 2
desktop/libs/dashboard/src/dashboard/data_export.py

@@ -28,7 +28,7 @@ LOG = logging.getLogger(__name__)
 DL_FORMATS = [ 'csv', 'xls' ]
 DL_FORMATS = [ 'csv', 'xls' ]
 
 
 
 
-def download(results, format, collection):
+def download(results, format, collection, user_agent=None):
   """
   """
   download(results, format) -> HttpResponse
   download(results, format) -> HttpResponse
 
 
@@ -40,7 +40,7 @@ def download(results, format, collection):
 
 
   content_generator = SearchDataAdapter(results, format, collection)
   content_generator = SearchDataAdapter(results, format, collection)
   generator = export_csvxls.create_generator(content_generator, format)
   generator = export_csvxls.create_generator(content_generator, format)
-  return export_csvxls.make_response(generator, format, 'query_result')
+  return export_csvxls.make_response(generator, format, 'query_result', user_agent=user_agent)
 
 
 
 
 def SearchDataAdapter(results, format, collection):
 def SearchDataAdapter(results, format, collection):

+ 1 - 1
desktop/libs/notebook/src/notebook/connectors/base.py

@@ -312,7 +312,7 @@ class Api(object):
   def fetch_result_size(self, notebook, snippet):
   def fetch_result_size(self, notebook, snippet):
     raise OperationNotSupported()
     raise OperationNotSupported()
 
 
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
     pass
     pass
 
 
   def get_log(self, notebook, snippet, startFrom=None, size=None):
   def get_log(self, notebook, snippet, startFrom=None, size=None):

+ 2 - 2
desktop/libs/notebook/src/notebook/connectors/hiveserver2.py

@@ -369,7 +369,7 @@ class HS2Api(Api):
 
 
 
 
   @query_error_handler
   @query_error_handler
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
     try:
     try:
       db = self._get_db(snippet)
       db = self._get_db(snippet)
       handle = self._get_handle(snippet)
       handle = self._get_handle(snippet)
@@ -378,7 +378,7 @@ class HS2Api(Api):
 
 
       file_name = _get_snippet_name(notebook)
       file_name = _get_snippet_name(notebook)
 
 
-      return data_export.download(handle, format, db, id=snippet['id'], file_name=file_name)
+      return data_export.download(handle, format, db, id=snippet['id'], file_name=file_name, user_agent=user_agent)
     except Exception, e:
     except Exception, e:
       title = 'The query result cannot be downloaded.'
       title = 'The query result cannot be downloaded.'
       LOG.exception(title)
       LOG.exception(title)

+ 1 - 1
desktop/libs/notebook/src/notebook/connectors/jdbc.py

@@ -123,7 +123,7 @@ class JdbcApi(Api):
   def cancel(self, notebook, snippet):
   def cancel(self, notebook, snippet):
     return {'status': 0}
     return {'status': 0}
 
 
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
     raise PopupException('Downloading is not supported yet')
     raise PopupException('Downloading is not supported yet')
 
 
   def progress(self, snippet, logs):
   def progress(self, snippet, logs):

+ 2 - 2
desktop/libs/notebook/src/notebook/connectors/rdbms.py

@@ -108,13 +108,13 @@ class RdbmsApi(Api):
 
 
 
 
   @query_error_handler
   @query_error_handler
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
 
 
     file_name = _get_snippet_name(notebook)
     file_name = _get_snippet_name(notebook)
     results = self._execute(notebook, snippet)
     results = self._execute(notebook, snippet)
     db = FixedResult(results)
     db = FixedResult(results)
 
 
-    return data_export.download(None, format, db, id=snippet['id'], file_name=file_name)
+    return data_export.download(None, format, db, id=snippet['id'], file_name=file_name, user_agent=user_agent)
 
 
 
 
   @query_error_handler
   @query_error_handler

+ 1 - 1
desktop/libs/notebook/src/notebook/connectors/solr.py

@@ -129,7 +129,7 @@ class SolrApi(Api):
     return 'No logs'
     return 'No logs'
 
 
 
 
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
     raise PopupException('Downloading is not supported yet')
     raise PopupException('Downloading is not supported yet')
 
 
 
 

+ 2 - 2
desktop/libs/notebook/src/notebook/connectors/spark_shell.py

@@ -326,13 +326,13 @@ class SparkApi(Api):
 
 
       raise QueryError(msg)
       raise QueryError(msg)
 
 
-  def download(self, notebook, snippet, format):
+  def download(self, notebook, snippet, format, user_agent=None):
     try:
     try:
       api = get_spark_api(self.user)
       api = get_spark_api(self.user)
       session = _get_snippet_session(notebook, snippet)
       session = _get_snippet_session(notebook, snippet)
       cell = snippet['result']['handle']['id']
       cell = snippet['result']['handle']['id']
 
 
-      return spark_download(api, session['id'], cell, format)
+      return spark_download(api, session['id'], cell, format, user_agent=None)
     except Exception, e:
     except Exception, e:
       raise PopupException(e)
       raise PopupException(e)
 
 

+ 2 - 2
desktop/libs/notebook/src/notebook/data_export.py

@@ -27,14 +27,14 @@ LOG = logging.getLogger(__name__)
 DL_FORMATS = [ 'csv', 'xls' ]
 DL_FORMATS = [ 'csv', 'xls' ]
 
 
 
 
-def download(api, session, cell, format):
+def download(api, session, cell, format, user_agent=None):
   if format not in DL_FORMATS:
   if format not in DL_FORMATS:
     LOG.error('Unknown download format "%s"' % format)
     LOG.error('Unknown download format "%s"' % format)
     return
     return
 
 
   content_generator = SparkDataAdapter(api, session, cell)
   content_generator = SparkDataAdapter(api, session, cell)
   generator = export_csvxls.create_generator(content_generator, format)
   generator = export_csvxls.create_generator(content_generator, format)
-  return export_csvxls.make_response(generator, format, 'script_result')
+  return export_csvxls.make_response(generator, format, 'script_result', user_agent=user_agent)
 
 
 
 
 def SparkDataAdapter(api, session, cell):
 def SparkDataAdapter(api, session, cell):

+ 1 - 1
desktop/libs/notebook/src/notebook/views.py

@@ -317,7 +317,7 @@ def download(request):
   snippet = json.loads(request.POST.get('snippet', '{}'))
   snippet = json.loads(request.POST.get('snippet', '{}'))
   file_format = request.POST.get('format', 'csv')
   file_format = request.POST.get('format', 'csv')
 
 
-  response = get_api(request, snippet).download(notebook, snippet, file_format)
+  response = get_api(request, snippet).download(notebook, snippet, file_format, user_agent=request.META.get('HTTP_USER_AGENT'))
 
 
   if response:
   if response:
     request.audit = {
     request.audit = {