diff --git a/tests/test_api_client/test_file_download.py b/tests/test_api_client/test_file_download.py new file mode 100644 index 00000000..9be78ee0 --- /dev/null +++ b/tests/test_api_client/test_file_download.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import pytest + +from xero_python.api_client import ApiClient +from xero_python.api_client.configuration import Configuration + + +class FakeResponse: + def __init__(self, content_disposition, data=b"file contents"): + self.content_disposition = content_disposition + self.data = data + + def getheader(self, name): + if name == "Content-Disposition": + return self.content_disposition + return None + + +@pytest.fixture +def api_client(tmp_path): + configuration = Configuration() + configuration.temp_folder_path = str(tmp_path) + return ApiClient(configuration=configuration) + + +def deserialize_file(api_client, response): + return Path(api_client._ApiClient__deserialize_file(response)) + + +def test_deserialize_file_keeps_download_within_temp_directory(api_client, tmp_path): + path = deserialize_file( + api_client, FakeResponse('attachment; filename="../outside.txt"') + ) + + assert path.parent == tmp_path + assert path.name == "outside.txt" + assert path.read_bytes() == b"file contents" + assert not (tmp_path.parent / "outside.txt").exists() + + +def test_deserialize_file_uses_content_disposition_filename(api_client, tmp_path): + path = deserialize_file( + api_client, FakeResponse('attachment; filename="report.csv"') + ) + + assert path == tmp_path / "report.csv" + assert path.read_bytes() == b"file contents" + + +def test_deserialize_file_uses_generated_filename_without_filename_parameter( + api_client, tmp_path +): + path = deserialize_file(api_client, FakeResponse("inline")) + + assert path.parent == tmp_path + assert path.read_bytes() == b"file contents" diff --git a/xero_python/api_client/__init__.py b/xero_python/api_client/__init__.py index 645cf506..3417979e 100644 --- a/xero_python/api_client/__init__.py +++ b/xero_python/api_client/__init__.py @@ -603,10 +603,15 @@ def __deserialize_file(self, response): content_disposition = response.getheader("Content-Disposition") if content_disposition: - filename = re.search( - r'filename=[\'"]?([^\'"\s]+)[\'"]?', content_disposition - ).group(1) - path = os.path.join(os.path.dirname(path), filename) + match = re.search( + r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition, + flags=re.IGNORECASE, + ) + if match: + filename = os.path.basename(match.group(1)) + if filename not in ("", ".", ".."): + path = os.path.join(os.path.dirname(path), filename) with open(path, "wb") as f: f.write(response.data)