Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/seven-bears-stick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gradio": patch
---

fix:Stage uploads on the cache filesystem
Comment on lines +1 to +5
9 changes: 7 additions & 2 deletions gradio/route_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,8 @@ class GradioMultiPartParser:

Made the following modifications
- Use GradioUploadFile instead of UploadFile
- Use NamedTemporaryFile instead of SpooledTemporaryFile
- Use NamedTemporaryFile instead of SpooledTemporaryFile, optionally
placing it in Gradio's upload directory
- Compute hash of data as the request is streamed

"""
Expand All @@ -700,6 +701,7 @@ def __init__(
*,
max_files: Union[int, float] = 1000,
max_fields: Union[int, float] = 1000,
upload_dir: str | Path | None = None,
upload_id: str | None = None,
upload_progress: FileUploadProgress | None = None,
max_file_size: int | float,
Expand All @@ -709,6 +711,7 @@ def __init__(
self.stream = stream
self.max_files = max_files
self.max_fields = max_fields
self.upload_dir = upload_dir
self.items: list[tuple[str, Union[str, UploadFile]]] = []
self.upload_id = upload_id
self.upload_progress = upload_progress
Expand Down Expand Up @@ -804,7 +807,7 @@ def on_headers_finished(self) -> None:
f"Too many files. Maximum number of files is {self.max_files}."
)
filename = _user_safe_decode(options[b"filename"], str(self._charset))
tempfile = NamedTemporaryFile(delete=False)
tempfile = NamedTemporaryFile(delete=False, dir=self.upload_dir)
self._files_to_close_on_error.append(tempfile)
self._current_part.file = GradioUploadFile(
file=tempfile, # type: ignore[arg-type]
Expand Down Expand Up @@ -1542,6 +1545,7 @@ async def upload_fn(
if content_type != b"multipart/form-data":
raise HTTPException(status_code=400, detail="Invalid content type.")

Path(upload_dir).mkdir(exist_ok=True, parents=True)
if upload_id and upload_progress:
upload_progress.track(upload_id)

Expand All @@ -1550,6 +1554,7 @@ async def upload_fn(
request.stream(),
max_files=1000,
max_fields=1000,
upload_dir=upload_dir,
max_file_size=max_file_size,
upload_id=upload_id,
upload_progress=upload_progress,
Expand Down
32 changes: 32 additions & 0 deletions test/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,38 @@ def test_custom_upload_path(self, gradio_temp_dir):
with open(file, "rb") as saved_file:
assert saved_file.read() == b"abcdefghijklmnopqrstuvwxyz"

def test_upload_is_staged_in_custom_upload_path(self, gradio_temp_dir, monkeypatch):
blocks = Blocks()
blocks.max_file_size = None
blocks.upload_file_set = set()
blocks.share = False
app = routes.App.create_app(blocks)
test_client = TestClient(app)
original_rename = os.rename
staged_paths = []

def record_rename(source, destination):
source = Path(source).resolve()
staged_paths.append(source)
if not source.is_relative_to(gradio_temp_dir.resolve()):
raise OSError("simulated cross-filesystem rename")
return original_rename(source, destination)

def reject_background_move(*_args):
raise AssertionError("upload must be published before the response")

monkeypatch.setattr(os, "rename", record_rename)
monkeypatch.setattr(
routes, "move_uploaded_files_to_cache", reject_background_move
)
with open("test/test_files/alphabet.txt", "rb") as file:
response = test_client.post(f"{API_PREFIX}/upload", files={"files": file})

assert response.status_code == 200
assert len(staged_paths) == 1
assert staged_paths[0].is_relative_to(gradio_temp_dir.resolve())
assert Path(response.json()[0]).read_bytes() == b"abcdefghijklmnopqrstuvwxyz"

@pytest.mark.skipif(
sys.platform == "win32",
reason="On Windows CI python_multipart raises MultipartParseError while "
Expand Down
Loading