瀏覽代碼

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
Elijah Ahianyo 2 年之前
父節點
當前提交
a2f86f9fbb
共有 5 個文件被更改,包括 71 次插入14 次删除
  1. 7 4
      pynecone/.templates/web/utils/state.js
  2. 11 6
      pynecone/app.py
  3. 8 1
      pynecone/utils/format.py
  4. 29 1
      tests/conftest.py
  5. 16 2
      tests/test_utils.py

+ 7 - 4
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) => {

+ 11 - 6
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
 

+ 8 - 1
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]:

+ 29 - 1
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

+ 16 - 2
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)"
     )