From e9c0f6a72c7599b5254bd962c7c04013d788d626 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:43:11 +0530 Subject: [PATCH 1/4] fix: isolate concurrent ZIP upload temp files --- backend/api_upload.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/api_upload.py b/backend/api_upload.py index 3663370..194f148 100644 --- a/backend/api_upload.py +++ b/backend/api_upload.py @@ -1,3 +1,4 @@ +import tempfile from pathlib import Path from fastapi import APIRouter, File, HTTPException, UploadFile from services.github import extract_zip @@ -5,16 +6,27 @@ router = APIRouter() + +def write_temp_upload(data: bytes) -> Path: + with tempfile.NamedTemporaryFile( + prefix="repopilot-", + suffix=".zip", + delete=False, + ) as handle: + handle.write(data) + return Path(handle.name) + + @router.post("/api/upload") async def upload_repository(file: UploadFile = File(...)): if not file.filename or not file.filename.lower().endswith(".zip"): raise HTTPException(400, "Upload a .zip repository archive.") - temp = Path("/tmp") / f"repopilot-{Path(file.filename).name}" + temp = None try: data = await file.read(50 * 1024 * 1024 + 1) - temp.write_bytes(data) if len(data) > 50 * 1024 * 1024: raise ValueError("ZIP is too large. Maximum upload size is 50 MB.") + temp = write_temp_upload(data) repo_id, path = extract_zip(temp) result = analyze(path) result.update(repo_id=repo_id, source_url=f"ZIP: {file.filename}") @@ -24,4 +36,5 @@ async def upload_repository(file: UploadFile = File(...)): except Exception as e: raise HTTPException(500, f"Could not analyze ZIP: {e}") finally: - temp.unlink(missing_ok=True) + if temp is not None: + temp.unlink(missing_ok=True) From 84da2cdddb2a08bd280b233a2cccba918d07492a Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:43:13 +0530 Subject: [PATCH 2/4] test: cover upload tempfile isolation --- backend/tests/test_upload_tempfile.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 backend/tests/test_upload_tempfile.py diff --git a/backend/tests/test_upload_tempfile.py b/backend/tests/test_upload_tempfile.py new file mode 100644 index 0000000..491b39a --- /dev/null +++ b/backend/tests/test_upload_tempfile.py @@ -0,0 +1,20 @@ +import unittest + +from api_upload import write_temp_upload + + +class UploadTempFileTests(unittest.TestCase): + def test_each_upload_gets_a_unique_temp_file(self): + first = write_temp_upload(b"first") + second = write_temp_upload(b"second") + try: + self.assertNotEqual(first, second) + self.assertEqual(first.read_bytes(), b"first") + self.assertEqual(second.read_bytes(), b"second") + finally: + first.unlink(missing_ok=True) + second.unlink(missing_ok=True) + + +if __name__ == "__main__": + unittest.main() From 3ba1d2c0654c21fd0333303189afcda634cd5b76 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:46:46 +0530 Subject: [PATCH 3/4] fix: stream ZIP uploads into isolated temp files --- backend/api_upload.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/backend/api_upload.py b/backend/api_upload.py index 194f148..c12994d 100644 --- a/backend/api_upload.py +++ b/backend/api_upload.py @@ -6,15 +6,35 @@ router = APIRouter() +MAX_UPLOAD_BYTES = 50 * 1024 * 1024 +UPLOAD_CHUNK_BYTES = 1024 * 1024 -def write_temp_upload(data: bytes) -> Path: - with tempfile.NamedTemporaryFile( - prefix="repopilot-", - suffix=".zip", - delete=False, - ) as handle: - handle.write(data) - return Path(handle.name) + +async def write_temp_upload(file: UploadFile) -> Path: + path = None + try: + with tempfile.NamedTemporaryFile( + prefix="repopilot-", + suffix=".zip", + delete=False, + ) as handle: + path = Path(handle.name) + total = 0 + while True: + chunk = await file.read(UPLOAD_CHUNK_BYTES) + if not chunk: + break + total += len(chunk) + if total > MAX_UPLOAD_BYTES: + raise ValueError( + "ZIP is too large. Maximum upload size is 50 MB." + ) + handle.write(chunk) + return path + except Exception: + if path is not None: + path.unlink(missing_ok=True) + raise @router.post("/api/upload") @@ -23,10 +43,7 @@ async def upload_repository(file: UploadFile = File(...)): raise HTTPException(400, "Upload a .zip repository archive.") temp = None try: - data = await file.read(50 * 1024 * 1024 + 1) - if len(data) > 50 * 1024 * 1024: - raise ValueError("ZIP is too large. Maximum upload size is 50 MB.") - temp = write_temp_upload(data) + temp = await write_temp_upload(file) repo_id, path = extract_zip(temp) result = analyze(path) result.update(repo_id=repo_id, source_url=f"ZIP: {file.filename}") From 1671fe45497faa60dbca1f7e0a62162532d5b8ba Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:46:49 +0530 Subject: [PATCH 4/4] test: cover streamed upload isolation and cleanup --- backend/tests/test_upload_tempfile.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_upload_tempfile.py b/backend/tests/test_upload_tempfile.py index 491b39a..8f37d54 100644 --- a/backend/tests/test_upload_tempfile.py +++ b/backend/tests/test_upload_tempfile.py @@ -1,12 +1,22 @@ +import asyncio +import io +import tempfile import unittest +from pathlib import Path +from unittest.mock import patch + +from fastapi import UploadFile from api_upload import write_temp_upload class UploadTempFileTests(unittest.TestCase): + def make_upload(self, data): + return UploadFile(filename="repository.zip", file=io.BytesIO(data)) + def test_each_upload_gets_a_unique_temp_file(self): - first = write_temp_upload(b"first") - second = write_temp_upload(b"second") + first = asyncio.run(write_temp_upload(self.make_upload(b"first"))) + second = asyncio.run(write_temp_upload(self.make_upload(b"second"))) try: self.assertNotEqual(first, second) self.assertEqual(first.read_bytes(), b"first") @@ -15,6 +25,20 @@ def test_each_upload_gets_a_unique_temp_file(self): first.unlink(missing_ok=True) second.unlink(missing_ok=True) + def test_oversized_upload_is_removed_after_streaming_fails(self): + with tempfile.TemporaryDirectory() as temp_dir: + with ( + patch("api_upload.MAX_UPLOAD_BYTES", 4), + patch("api_upload.tempfile.tempdir", temp_dir), + ): + with self.assertRaisesRegex(ValueError, "too large"): + asyncio.run( + write_temp_upload(self.make_upload(b"12345")) + ) + + leftovers = list(Path(temp_dir).glob("repopilot-*.zip")) + self.assertEqual(leftovers, []) + if __name__ == "__main__": unittest.main()