patx/gitman

add limits to work with bottles multipart parser

Commit e3e4904 · patx · 2026-05-06T22:00:09-04:00

Changeset
e3e4904dc4d80741171c0a1e781407ecce535642
Parents
ae45d36aeeb2f41de09bca1b2d957a88a458f9d3

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/app.py b/app.py
index 0063f87..4657e80 100644
--- a/app.py
+++ b/app.py
@@ -65,6 +65,7 @@ PASSWORD_ITERATIONS = 260_000
 SQLITE_BUSY_TIMEOUT_MS = 30_000
 MAX_FORM_BYTES = env_int("GITMAN_MAX_FORM_BYTES", 64 * 1024)
 MAX_IMPORT_BYTES = env_int("GITMAN_MAX_IMPORT_BYTES", 5 * 1024 * 1024 * 1024)
+IMPORT_UPLOAD_CHUNK_BYTES = 1024 * 1024
 GIT_IMPORT_TIMEOUT_SECONDS = env_int("GITMAN_IMPORT_TIMEOUT_SECONDS", 3600, minimum=1)
 MAX_RENDER_BYTES = env_int("GITMAN_MAX_RENDER_BYTES", 256 * 1024)
 MAX_GIT_RESPONSE_BYTES = env_int("GITMAN_MAX_GIT_RESPONSE_BYTES", 256 * 1024 * 1024)
@@ -1490,15 +1491,20 @@ def save_bundle_upload(upload, destination):
     except (AttributeError, OSError):
         pass
 
-    with destination.open("wb") as target:
-        while True:
-            chunk = source.read(1024 * 1024)
-            if not chunk:
-                break
-            size += len(chunk)
-            if MAX_IMPORT_BYTES and size > MAX_IMPORT_BYTES:
-                raise UploadTooLarge("Request body too large.")
-            target.write(chunk)
+    try:
+        with destination.open("wb") as target:
+            while True:
+                chunk = source.read(IMPORT_UPLOAD_CHUNK_BYTES)
+                if not chunk:
+                    break
+                size += len(chunk)
+                if MAX_IMPORT_BYTES and size > MAX_IMPORT_BYTES:
+                    raise UploadTooLarge("Request body too large.")
+                target.write(chunk)
+    except UploadTooLarge:
+        if destination.exists():
+            destination.unlink()
+        raise
 
     if size == 0:
         raise ValueError("Uploaded bundle is empty.")
diff --git a/tests/test_app.py b/tests/test_app.py
index 4cf6d0d..2417733 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -841,6 +841,34 @@ def test_import_git_bundle_rejects_invalid_upload_without_replacing_repo(isolate
     assert path.joinpath("HEAD").read_text(encoding="utf-8").strip() == "ref: refs/heads/main"
 
 
+def test_save_bundle_upload_uses_import_chunk_size_and_removes_oversized_partial(tmp_path, monkeypatch):
+    class RecordingSource(BytesIO):
+        def __init__(self, content):
+            super().__init__(content)
+            self.read_sizes = []
+
+        def read(self, size=-1):
+            self.read_sizes.append(size)
+            return super().read(size)
+
+    class Upload:
+        filename = "repo.bundle"
+
+        def __init__(self, content):
+            self.file = RecordingSource(content)
+
+    monkeypatch.setattr(gitman, "MAX_IMPORT_BYTES", 10)
+    monkeypatch.setattr(gitman, "IMPORT_UPLOAD_CHUNK_BYTES", 4)
+    upload = Upload(b"x" * 12)
+    destination = tmp_path / "repo.bundle"
+
+    with pytest.raises(gitman.UploadTooLarge):
+        gitman.save_bundle_upload(upload, destination)
+
+    assert upload.file.read_sizes == [4, 4, 4]
+    assert not destination.exists()
+
+
 def test_import_git_bundle_size_limit_returns_413(isolated_app, monkeypatch):
     monkeypatch.setattr(gitman, "MAX_IMPORT_BYTES", 100)
     owner = create_user("alice")