diff --git a/reflex/docs/api-routes/overview.md b/reflex/docs/api-routes/overview.md index e95f285d4e0..51a160b9e1a 100644 --- a/reflex/docs/api-routes/overview.md +++ b/reflex/docs/api-routes/overview.md @@ -160,4 +160,6 @@ The expected return is `"pong"`. ### Upload -`localhost:8000/_upload`: This route is used for the upload of file when using `rx.upload()`. +`localhost:8000/_upload`: This route is used for file uploads when using +`rx.upload()`, including `rx.upload_files(...)` and +`rx.upload_files_chunk(...)`. diff --git a/reflex/docs/library/forms/upload.md b/reflex/docs/library/forms/upload.md index 63479d51e76..7ea40bad823 100644 --- a/reflex/docs/library/forms/upload.md +++ b/reflex/docs/library/forms/upload.md @@ -51,8 +51,11 @@ on the frontend using the `rx.selected_files(id)` special Var. To clear the selected files, you can use another special Var `rx.clear_selected_files(id)` as an event handler. -To upload the file(s), you need to bind an event handler and pass the special -`rx.upload_files(upload_id=id)` event arg to it. +To upload the file(s), bind an event handler and pass one of these special +event args: + +- `rx.upload_files(upload_id=id)` for uploads +- `rx.upload_files_chunk(upload_id=id)` for larger files that should be processed incrementally ## File Storage Functions @@ -422,9 +425,7 @@ rx.upload.root( ## Handling the Upload -Your event handler should be an async function that accepts a single argument, -`files: list[UploadFile]`, which will contain [FastAPI UploadFile](https://fastapi.tiangolo.com/tutorial/request-files) instances. -You can read the files and save them anywhere as shown in the example. +For uploads, your event handler should be an async function that accepts a single argument, `files: list[UploadFile]`, which will contain [Starlette UploadFile](https://www.starlette.io/requests/#request-files) instances. You can read the files and save them anywhere as shown in the example. In your UI, you can bind the event handler to a trigger, such as a button `on_click` event or upload `on_drop` event, and pass in the files using @@ -459,13 +460,105 @@ The files are automatically served at: - `/_upload/document.pdf` ← `rx.get_upload_url("document.pdf")` - `/_upload/video.mp4` ← `rx.get_upload_url("video.mp4")` +### Chunked Uploads for Large Files + +Use `rx.upload_files_chunk(...)` when files may be large or when you want the backend to write data incrementally. Standard uploads spool files to disk before the handler starts, but calling `await file.read()` in the handler loads the entire file into memory at once, which can cause high memory consumption for large files. + +Chunked upload handlers: + +- must be declared with `@rx.event(background=True)` +- must accept `chunk_iter: rx.UploadChunkIterator` +- must fully consume `chunk_iter` + +To use chunked uploads in your own app: + +1. Create an `@rx.event(background=True)` handler. +2. Accept `chunk_iter: rx.UploadChunkIterator`. +3. Iterate over the chunks and write `chunk.data` at `chunk.offset`. +4. Trigger the handler with `rx.upload_files_chunk(upload_id=...)`. + +Each chunk includes: + +- `chunk.filename` +- `chunk.offset` +- `chunk.content_type` +- `chunk.data` + +```python +class ChunkUploadState(rx.State): + uploaded_files: list[str] = [] + status: str = "No chunked upload has finished yet." + + @rx.event(background=True) + async def handle_large_upload(self, chunk_iter: rx.UploadChunkIterator): + file_handles = {} + destinations = {} + + try: + async with self: + self.status = "Streaming upload in progress." + async for chunk in chunk_iter: + path = destinations.setdefault( + chunk.filename, + rx.get_upload_dir() / "stream" / chunk.filename, + ) + path.parent.mkdir(parents=True, exist_ok=True) + + fh = file_handles.get(chunk.filename) + if fh is None: + fh = path.open("wb") + file_handles[chunk.filename] = fh + + fh.seek(chunk.offset) + fh.write(chunk.data) + finally: + for fh in file_handles.values(): + fh.close() + + async with self: + self.uploaded_files = sorted(destinations) + self.status = "Chunked upload complete." + + +def chunked_upload_component(): + return rx.vstack( + rx.upload( + rx.text("Drop files here or click to select"), + id="large_upload", + border="2px dashed #ccc", + padding="2em", + ), + rx.button( + "Upload Large Files", + on_click=ChunkUploadState.handle_large_upload( + rx.upload_files_chunk(upload_id="large_upload") + ), + ), + rx.text(ChunkUploadState.status), + rx.foreach( + ChunkUploadState.uploaded_files, + lambda filename: rx.text(filename), + ), + ) +``` + +Returning early from the handler will fail the upload because the remaining +chunks were not consumed. + +If you want a progress bar or a cancel button, `rx.upload_files_chunk(...)` +supports the same `on_upload_progress` callback as uploads, and +you can stop the upload with `rx.cancel_upload(upload_id)`. + ## Cancellation The `id` provided to the `rx.upload` component can be passed to the special event handler `rx.cancel_upload(id)` to stop uploading on demand. Cancellation can be triggered directly by a frontend event trigger, or it can be returned from a backend event handler. ## Progress -The `rx.upload_files` special event arg also accepts an `on_upload_progress` event trigger which will be fired about every second during the upload operation to report the progress of the upload. This can be used to update a progress bar or other UI elements to show the user the progress of the upload. +Both `rx.upload_files` and `rx.upload_files_chunk` accept an +`on_upload_progress` event trigger which will be fired during the upload +operation to report the progress of the upload. This can be used to update a +progress bar or other UI elements to show the user the progress of the upload. ```python class UploadExample(rx.State): @@ -521,15 +614,10 @@ def upload_form(): The `progress` dictionary contains the following keys: -```javascript -\{ - 'loaded': 36044800, - 'total': 54361908, - 'progress': 0.6630525183185255, - 'bytes': 20447232, - 'rate': None, - 'estimated': None, - 'event': \{'isTrusted': True}, - 'upload': True +```python +{ + "loaded": 36044800, + "total": 54361908, + "progress": 0.6630525183185255, } ```