Skip to content

Commit 716506c

Browse files
tdruezchinyeungli
authored andcommitted
fix: serve files as_attachment only when too large (#2223)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 3d2f059 commit 716506c

3 files changed

Lines changed: 32 additions & 7 deletions

File tree

scanpipe/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
# Syntax in .env: SCANCODEIO_GLOBAL_WEBHOOK=target_url=https://webhook.url,
6666
# trigger_on_each_run=False,include_summary=True,include_results=False
6767
"GLOBAL_WEBHOOK": {},
68+
# Maximum file size, in bytes, served inline in the browser rather than
69+
# forced as an attachment download. Above this size, the browser tab
70+
# rendering the file (e.g. a large JSON) risks hanging.
71+
"INLINE_DOWNLOAD_MAX_SIZE": 10_000_000,
6872
# Default limit for "most common" entries in QuerySets.
6973
"MOST_COMMON_LIMIT": 7,
7074
# Syntax in .env: SCANCODEIO_NETRC_LOCATION="~/.netrc"

scanpipe/tests/test_views.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ def test_scanpipe_views_project_details_download_input_view(self):
364364
response = self.client.get(url)
365365
self.assertTrue(response.getvalue().startswith(b"# SPDX-License-Identifier"))
366366
self.assertEqual("application/octet-stream", response.headers["Content-Type"])
367+
self.assertEqual(
368+
'inline; filename="notice.NOTICE"',
369+
response.headers["Content-Disposition"],
370+
)
371+
372+
with override_settings(SCANPIPE={"INLINE_DOWNLOAD_MAX_SIZE": 0}):
373+
response = self.client.get(url)
367374
self.assertEqual(
368375
'attachment; filename="notice.NOTICE"',
369376
response.headers["Content-Disposition"],
@@ -381,6 +388,13 @@ def test_scanpipe_views_project_details_download_output_view(self):
381388
response = self.client.get(url)
382389
self.assertTrue(response.getvalue().startswith(b"# SPDX-License-Identifier"))
383390
self.assertEqual("application/octet-stream", response.headers["Content-Type"])
391+
self.assertEqual(
392+
'inline; filename="notice.NOTICE"',
393+
response.headers["Content-Disposition"],
394+
)
395+
396+
with override_settings(SCANPIPE={"INLINE_DOWNLOAD_MAX_SIZE": 0}):
397+
response = self.client.get(url)
384398
self.assertEqual(
385399
'attachment; filename="notice.NOTICE"',
386400
response.headers["Content-Disposition"],
@@ -1491,6 +1505,10 @@ def test_project_packages_export_json(self):
14911505

14921506
self.assertIsInstance(response, FileResponse)
14931507
self.assertEqual(response.get("Content-Type"), "application/json")
1508+
self.assertTrue(response.get("Content-Disposition").startswith("inline"))
1509+
1510+
with override_settings(SCANPIPE={"INLINE_DOWNLOAD_MAX_SIZE": 0}):
1511+
response = self.client.get(url + "?export_json=True")
14941512
self.assertTrue(response.get("Content-Disposition").startswith("attachment"))
14951513

14961514
file_content = b"".join(response.streaming_content).decode("utf-8")
@@ -1556,7 +1574,7 @@ def test_project_dependencies_export_json(self):
15561574

15571575
self.assertIsInstance(response, FileResponse)
15581576
self.assertEqual(response.get("Content-Type"), "application/json")
1559-
self.assertTrue(response.get("Content-Disposition").startswith("attachment"))
1577+
self.assertTrue(response.get("Content-Disposition").startswith("inline"))
15601578

15611579
file_content = b"".join(response.streaming_content).decode("utf-8")
15621580
json_data = json.loads(file_content)
@@ -1592,7 +1610,7 @@ def test_project_relations_export_json(self):
15921610

15931611
self.assertIsInstance(response, FileResponse)
15941612
self.assertEqual(response.get("Content-Type"), "application/json")
1595-
self.assertTrue(response.get("Content-Disposition").startswith("attachment"))
1613+
self.assertTrue(response.get("Content-Disposition").startswith("inline"))
15961614

15971615
file_content = b"".join(response.streaming_content).decode("utf-8")
15981616
json_data = json.loads(file_content)
@@ -1616,7 +1634,7 @@ def test_project_messages_export_json(self):
16161634

16171635
self.assertIsInstance(response, FileResponse)
16181636
self.assertEqual(response.get("Content-Type"), "application/json")
1619-
self.assertTrue(response.get("Content-Disposition").startswith("attachment"))
1637+
self.assertTrue(response.get("Content-Disposition").startswith("inline"))
16201638

16211639
file_content = b"".join(response.streaming_content).decode("utf-8")
16221640
json_data = json.loads(file_content)
@@ -1642,7 +1660,7 @@ def test_project_codebase_resources_export_json(self):
16421660

16431661
self.assertIsInstance(response, FileResponse)
16441662
self.assertEqual(response.get("Content-Type"), "application/json")
1645-
self.assertTrue(response.get("Content-Disposition").startswith("attachment"))
1663+
self.assertTrue(response.get("Content-Disposition").startswith("inline"))
16461664

16471665
file_content = b"".join(response.streaming_content).decode("utf-8")
16481666
json_data = json.loads(file_content)

scanpipe/views.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -540,11 +540,13 @@ def export_json_file_response(self):
540540
serializer = serializer_class(queryset, many=True)
541541
serialized_data = json.dumps(serializer.data, indent=2, cls=DjangoJSONEncoder)
542542

543-
output_file = io.BytesIO(serialized_data.encode("utf-8"))
543+
encoded_data = serialized_data.encode("utf-8")
544+
output_file = io.BytesIO(encoded_data)
545+
is_too_large = len(encoded_data) > scanpipe_settings.INLINE_DOWNLOAD_MAX_SIZE
544546

545547
return FileResponse(
546548
output_file,
547-
as_attachment=True,
549+
as_attachment=is_too_large,
548550
filename=self.get_export_json_filename(),
549551
content_type="application/json",
550552
)
@@ -1532,7 +1534,8 @@ def download_project_file(request, slug, filename, path_type):
15321534
if not file_path.exists():
15331535
raise Http404(f"{file_path} not found")
15341536

1535-
return FileResponse(file_path.open("rb"), as_attachment=True)
1537+
is_too_large = file_path.stat().st_size > scanpipe_settings.INLINE_DOWNLOAD_MAX_SIZE
1538+
return FileResponse(file_path.open("rb"), as_attachment=is_too_large)
15361539

15371540

15381541
@conditional_login_required

0 commit comments

Comments
 (0)