improve handling of multipart data. no longer auto saves to disk.

Commit b254d0e · patx · 2025-06-10T04:37:27-04:00

Changeset
b254d0eacfd00bd90dd51c8d501f0fea5049af53
Parents
9a301ac9f34d79b6f038985d274d6e48ceb9611e

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/MicroPie.py b/MicroPie.py
index c041e80..3975bea 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -34,7 +34,6 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 import asyncio
 import contextvars
 import inspect
-import os
 import re
 import time
 import uuid
@@ -54,7 +53,6 @@ except ImportError:
     JINJA_INSTALLED = False
 
 try:
-    import aiofiles, aiofiles.os
     from multipart import PushMultipartParser, MultipartSegment
     MULTIPART_INSTALLED = True
 except ImportError:
@@ -400,9 +398,8 @@ class App:
         Asynchronously parses a multipart form-data request.
 
         This method processes incoming multipart form-data, handling
-        both text fields and file uploads. It reads data from the provided
-        asyncio stream reader and extracts form values and files,
-        saving uploaded files to a designated directory.
+        both text fields and file uploads. File data is streamed to the handler
+        via an asyncio.Queue.
 
         Args:
             reader (asyncio.StreamReader): The stream reader from which
@@ -411,11 +408,11 @@ class App:
                 fields in the multipart request.
 
         Returns:
-            tuple[dict, dict]: A tuple containing form_data & files.
+            tuple[dict, dict]: A tuple containing form_data and files.
         """
         if not MULTIPART_INSTALLED:
-            print("For multipart form data support install 'multipart' and 'aiofiles'.")
-            await self._send_response(send, 500, "500 Internal Server Error")
+            print("For multipart form data support install 'multipart'.")
+            await self._send_response(None, 500, "500 Internal Server Error")
             return
 
         with PushMultipartParser(boundary) as parser:
@@ -424,10 +421,8 @@ class App:
             current_field_name: Optional[str] = None
             current_filename: Optional[str] = None
             current_content_type: Optional[str] = None
-            current_file: Optional[aiofiles.threadpool.binary.AsyncBufferedIOBase] = None
+            current_queue: Optional[asyncio.Queue] = None
             form_value: str = ""
-            upload_directory: str = "uploads"
-            await aiofiles.os.makedirs(upload_directory, exist_ok=True)
             while not parser.closed:
                 chunk: bytes = await reader.read(65536)
                 if not chunk:
@@ -438,39 +433,39 @@ class App:
                         current_filename = result.filename
                         current_content_type = None
                         form_value = ""
+                        if current_queue:
+                            await current_queue.put(None)  # Signal end of previous file stream
+                            current_queue = None
                         for header, value in result.headerlist:
                             if header.lower() == "content-type":
                                 current_content_type = value
-
                         if current_filename:
-                            safe_filename: str = f"{uuid.uuid4()}_{current_filename}"
-                            safe_filename = re.sub(r"[^a-zA-Z0-9_.-]", "_", safe_filename)
-                            file_path: str = os.path.join(upload_directory, safe_filename)
-                            current_file = await aiofiles.open(file_path, "wb")
+                            current_queue = asyncio.Queue()
+                            files[current_field_name] = {
+                                "filename": current_filename,
+                                "content_type": current_content_type or "application/octet-stream",
+                                "content": current_queue,
+                            }
                         else:
                             form_data[current_field_name] = []
                     elif result:
-                        if current_file:
-                            await current_file.write(result)
+                        if current_queue:
+                            await current_queue.put(result)
                         else:
-                            if current_file:
-                                form_value += result.decode("utf-8", "ignore")
+                            form_value += result.decode("utf-8", "ignore")
                     else:
-                        if current_file:
-                            await current_file.close()
-                            current_file = None
-                            files[current_field_name] = {
-                                "filename": current_filename,
-                                "content_type": current_content_type or "application/octet-stream",
-                                "saved_path": os.path.join(upload_directory, safe_filename),
-                            }
+                        if current_queue:
+                            await current_queue.put(None)  # Signal end of file stream
+                            current_queue = None
                         else:
-                            if form_value:
+                            if form_value and current_field_name:
                                 form_data[current_field_name].append(form_value)
                             form_value = ""
-            # Ensure any remaining form_value is appended
+            # Ensure any remaining form_value or file stream is processed
             if current_field_name and form_value and not current_filename:
                 form_data[current_field_name].append(form_value)
+            if current_queue:
+                await current_queue.put(None)  # Signal end of final file stream
             return form_data, files
 
     async def _send_response(
diff --git a/examples/file_uploads/app.py b/examples/file_uploads/app.py
index ddd4993..3efa239 100644
--- a/examples/file_uploads/app.py
+++ b/examples/file_uploads/app.py
@@ -1,12 +1,12 @@
-"""
-This file demonstrates how to use a middleware to check file upload sizes
-before the request body is processed by the multipart parser.
-"""
-
-from MicroPie import App, HttpMiddleware
+import os
+import aiofiles
+from MicroPie import App
 
+UPLOAD_DIR = "uploads"
+os.makedirs(UPLOAD_DIR, exist_ok=True)  # Ensure directory exists
 MAX_UPLOAD_SIZE = 100 * 1024 * 1024  # 100MB
 
+
 class MaxUploadSizeMiddleware(HttpMiddleware):
     async def before_request(self, request):
         # Check if we're dealing with a POST, PUT, or PATCH request
@@ -25,29 +25,31 @@ class MaxUploadSizeMiddleware(HttpMiddleware):
         return None
 
 
-class FileUploadApp(App):
+class Root(App):
+
     async def index(self):
-        """Serves an HTML form for file uploads."""
-        return """<html>
-<head><title>File Upload</title></head>
-<body>
-    <h2>Upload a File</h2>
-    <form action="/upload" method="post" enctype="multipart/form-data">
-        <label for="username">Username:</label><br>
-        <input type="text" id="username" name="username" required><br><br>
-        <label for="file">Select File:</label><br>
-        <input type="file" id="file" name="file" required><br><br>
-        <input type="submit" value="Upload">
-    </form>
-</body>
-</html>"""
+        return """<form action="/upload" method="post" enctype="multipart/form-data">
+            <label for="file">Choose a file:</label>
+            <input type="file" id="file" name="file" required>
+            <input type="submit" value="Upload">
+        </form>"""
 
     async def upload(self, file):
         filename = file["filename"]
-        saved_path = file["saved_path"]
-        username = self.request.body_params.get("username", ["Anonymous"])[0]
-        return f"File '{filename}' uploaded successfully by {username}, saved to: {saved_path}!"
+        queue = file["content"]
+        total_bytes = 0
+        filepath = os.path.join(UPLOAD_DIR, filename)
+
+        async with aiofiles.open(filepath, "wb") as f:
+            while True:
+                chunk = await queue.get()
+                if chunk is None:
+                    break
+                await f.write(chunk)
+                total_bytes += len(chunk)
+
+        return 200, f"Uploaded {filename} ({total_bytes} bytes) to {filepath}"
+
 
+app = Root()
 
-app = FileUploadApp()
-app.middlewares.insert(0, MaxUploadSizeMiddleware())