From a2f86f9fbb2cf6a4fb929b853581bfa4796ff63e Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Thu, 23 Mar 2023 06:54:08 +0000 Subject: [PATCH] Feature/Multi File upload (#712) * This PR adds a feature to upload multiple files * modified function to support python 3.8 * change code to use types instead of arg name --- pynecone/.templates/web/utils/state.js | 11 ++++++---- pynecone/app.py | 17 +++++++++------ pynecone/utils/format.py | 9 +++++++- tests/conftest.py | 30 +++++++++++++++++++++++++- tests/test_utils.py | 18 ++++++++++++++-- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index 333538037..496d8bc69 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/pynecone/.templates/web/utils/state.js @@ -192,6 +192,7 @@ export const connect = async ( * @param setResult The function to set the result. * @param files The files to upload. * @param handler The handler to use. + * @param multiUpload Whether handler args on backend is multiupload * @param endpoint The endpoint to upload to. */ export const uploadFiles = async ( @@ -200,6 +201,7 @@ export const uploadFiles = async ( setResult, files, handler, + multiUpload, endpoint ) => { // If we are already processing an event, or there are no upload files, return. @@ -210,15 +212,16 @@ export const uploadFiles = async ( // Set processing to true to block other events from being processed. setResult({ ...result, processing: true }); - // Currently only supports uploading one file. - const file = files[0]; + const name = multiUpload ? "files" : "file" const headers = { - "Content-Type": file.type, + "Content-Type": files[0].type, }; const formdata = new FormData(); // Add the token and handler to the file name. - formdata.append("file", file, getToken() + ":" + handler + ":" + file.name); + for (let i = 0; i < files.length; i++) { + formdata.append("files", files[i], getToken() + ":" + handler + ":" + name + ":" + files[i].name); + } // Send the file to the server. await axios.post(endpoint, formdata, headers).then((response) => { diff --git a/pynecone/app.py b/pynecone/app.py index 883654ad0..89e0273ee 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -465,22 +465,27 @@ def upload(app: App): The upload function. """ - async def upload_file(file: UploadFile): + async def upload_file(files: List[UploadFile]): """Upload a file. Args: - file: The file to upload. + files: The file(s) to upload. Returns: The state update after processing the event. """ - # Get the token and filename. - token, handler, filename = file.filename.split(":", 2) - file.filename = filename + token, handler, key = files[0].filename.split(":")[:3] + for file in files: + file.filename = file.filename.split(":")[-1] # Get the state for the session. state = app.state_manager.get_state(token) - event = Event(token=token, name=handler, payload={"file": file}) + # Event payload should have `files` as key for multi-uploads and `file` otherwise + event = Event( + token=token, + name=handler, + payload={key: files[0] if key == "file" else files}, + ) update = await state.process(event) return update diff --git a/pynecone/utils/format.py b/pynecone/utils/format.py index b42dd8551..1d813463f 100644 --- a/pynecone/utils/format.py +++ b/pynecone/utils/format.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import json import os import re @@ -298,9 +299,15 @@ def format_upload_event(event_spec: EventSpec) -> str: """ from pynecone.compiler import templates + multi_upload = any( + types._issubclass(arg_type, List) + for arg_type in inspect.getfullargspec( + event_spec.handler.fn + ).annotations.values() + ) state, name = get_event_handler_parts(event_spec.handler) parent_state = state.split(".")[0] - return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}", UPLOAD)' + return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",{str(multi_upload).lower()} ,UPLOAD)' def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]: diff --git a/tests/conftest.py b/tests/conftest.py index 1eadf0a3d..44b967e98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ """Test fixtures.""" import platform -from typing import Generator +from typing import Generator, List import pytest @@ -141,6 +141,7 @@ class UploadState(pc.State): """The base state for uploading a file.""" img: str + img_list: List[str] async def handle_upload(self, file: pc.UploadFile): """Handle the upload of a file. @@ -158,6 +159,23 @@ class UploadState(pc.State): # Update the img var. self.img = file.filename + async def multi_handle_upload(self, files: List[pc.UploadFile]): + """Handle the upload of a file. + + Args: + files: The uploaded files. + """ + for file in files: + upload_data = await file.read() + outfile = f".web/public/{file.filename}" + + # Save the file. + with open(outfile, "wb") as file_object: + file_object.write(upload_data) + + # Update the img var. + self.img_list.append(file.filename) + class BaseState(pc.State): """The test base state.""" @@ -197,3 +215,13 @@ def upload_sub_state_event_spec(): Event Spec. """ return EventSpec(handler=SubUploadState.handle_upload, upload=True) # type: ignore + + +@pytest.fixture +def multi_upload_event_spec(): + """Create an event Spec for a multi-upload base state. + + Returns: + Event Spec. + """ + return EventSpec(handler=UploadState.multi_handle_upload, upload=True) # type: ignore diff --git a/tests/test_utils.py b/tests/test_utils.py index b3450fa42..ea729ea47 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -296,7 +296,7 @@ def test_format_upload_event(upload_event_spec): assert ( format.format_upload_event(upload_event_spec) == "uploadFiles(upload_state, result, setResult, " - 'upload_state.files, "upload_state.handle_upload", ' + 'upload_state.files, "upload_state.handle_upload",false ,' "UPLOAD)" ) @@ -310,5 +310,19 @@ def test_format_sub_state_event(upload_sub_state_event_spec): assert ( format.format_upload_event(upload_sub_state_event_spec) == "uploadFiles(base_state, result, setResult, base_state.files, " - '"base_state.sub_upload_state.handle_upload", UPLOAD)' + '"base_state.sub_upload_state.handle_upload",false ,UPLOAD)' + ) + + +def test_format_multi_upload_event(multi_upload_event_spec): + """Test formatting an upload event spec. + + Args: + multi_upload_event_spec: The event spec fixture. + """ + assert ( + format.format_upload_event(multi_upload_event_spec) + == "uploadFiles(upload_state, result, setResult, " + 'upload_state.files, "upload_state.multi_handle_upload",true ,' + "UPLOAD)" )