patx/micropie
improve handling of multipart data. no longer auto saves to disk.
Commit b254d0e · patx · 2025-06-10T04:37:27-04:00
Comments
No comments yet.
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())