diff --git a/.gitignore b/.gitignore index f0226c065..e9bd2b49f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ var/ *.egg-info/ .installed.cfg *.egg +pip-wheel-metadata/ # PyInstaller # Usually these files are written by a python script from a template @@ -89,6 +90,7 @@ env.py # virtualenv venv/ ENV/ +.venv/ # Spyder project settings .spyderproject diff --git a/tableauserverclient/helpers/headers.py b/tableauserverclient/helpers/headers.py new file mode 100644 index 000000000..2ed4a814d --- /dev/null +++ b/tableauserverclient/helpers/headers.py @@ -0,0 +1,17 @@ +from copy import deepcopy +from urllib.parse import unquote_plus + + +def fix_filename(params): + if "filename*" not in params: + return params + + params = deepcopy(params) + filename = params["filename*"] + prefix = "UTF-8''" + if filename.startswith(prefix): + filename = filename[len(prefix) :] + + params["filename"] = unquote_plus(filename) + del params["filename*"] + return params diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index c60f8f919..66ad9f710 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union +from tableauserverclient.helpers.headers import fix_filename + if TYPE_CHECKING: from tableauserverclient.server import Server from tableauserverclient.models import PermissionsRule @@ -441,6 +443,7 @@ def download_revision( filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index ba8a152d7..21c16b1cc 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -7,6 +7,8 @@ from pathlib import Path from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union +from tableauserverclient.helpers.headers import fix_filename + from .dqw_endpoint import _DataQualityWarningEndpoint from .endpoint import QuerysetEndpoint, api from .exceptions import InternalServerError, MissingRequiredFieldError @@ -124,6 +126,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 3c8efbe3b..506fe02c2 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -6,6 +6,8 @@ from contextlib import closing from pathlib import Path +from tableauserverclient.helpers.headers import fix_filename + from .endpoint import QuerysetEndpoint, api, parameter_added_in from .exceptions import InternalServerError, MissingRequiredFieldError from .permissions_endpoint import _PermissionsEndpoint @@ -455,7 +457,7 @@ def _get_workbook_revisions( def download_revision( self, workbook_id: str, - revision_number: str, + revision_number: Optional[str], filepath: Optional[PathOrFileW] = None, include_extract: bool = True, no_extract: Optional[bool] = None, @@ -487,6 +489,7 @@ def download_revision( filepath.write(chunk) return_path = filepath else: + params = fix_filename(params) filename = to_filename(os.path.basename(params["filename"])) download_path = make_download_path(filepath, filename) with open(download_path, "wb") as f: diff --git a/test/test_datasource.py b/test/test_datasource.py index e299e5291..f258fdc52 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -696,3 +696,14 @@ def test_download_revision(self) -> None: ) file_path = self.server.datasources.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) self.assertTrue(os.path.exists(file_path)) + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={ + "Content-Disposition": '''name="tableau_datasource"; filename*=UTF-8''"Sample datasource.tds"''' + }, + ) + file_path = self.server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + self.assertTrue(os.path.exists(file_path)) diff --git a/test/test_flow.py b/test/test_flow.py index d10641809..a90b18171 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -1,5 +1,6 @@ import os import requests_mock +import tempfile import unittest from io import BytesIO @@ -203,3 +204,12 @@ def test_refresh(self): self.assertEqual(refresh_job.flow_run.id, "e0c3067f-2333-4eee-8028-e0a56ca496f6") self.assertEqual(refresh_job.flow_run.flow_id, "92967d2d-c7e2-46d0-8847-4802df58f484") self.assertEqual(format_datetime(refresh_job.flow_run.started_at), "2018-05-22T13:00:29Z") + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"'''}, + ) + file_path = self.server.flows.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + self.assertTrue(os.path.exists(file_path)) diff --git a/test/test_workbook.py b/test/test_workbook.py index 5114ce1b8..212d55a37 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -932,3 +932,12 @@ def test_download_revision(self) -> None: ) file_path = self.server.workbooks.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) self.assertTrue(os.path.exists(file_path)) + + def test_bad_download_response(self) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"'''}, + ) + file_path = self.server.workbooks.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + self.assertTrue(os.path.exists(file_path))