Skip to content

Commit 3e66f8e

Browse files
WIP
1 parent f0fb5e0 commit 3e66f8e

2 files changed

Lines changed: 145 additions & 20 deletions

File tree

blacksheep/docs/binders.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This page describes:
1414

1515
- [X] Implicit and explicit bindings.
1616
- [X] Built-in binders.
17+
- [X] Multipart/form-data support (improved in 2.6.0).
1718
- [X] How to define a custom binder.
1819

1920
It is recommended to read the following pages before this one:
@@ -350,6 +351,137 @@ def home(accept: FromAcceptHeader, foo: FromFooCookie) -> Response:
350351
)
351352
```
352353

354+
## Multipart/Form-data Support
355+
356+
/// admonition | Significantly improved in BlackSheep 2.6.0
357+
type: info
358+
359+
BlackSheep 2.6.0 introduces major improvements for handling `multipart/form-data` requests with the `FromForm` and `FromFiles` binders:
360+
361+
- **Memory-efficient file handling**: Files use `SpooledTemporaryFile` - small files (<1MB) stay in memory, larger files automatically spill to temporary disk files
362+
- **True streaming parsing**: New streaming API for processing multipart data without buffering the entire request body
363+
- **Automatic resource cleanup**: The framework automatically cleans up file resources at the end of each request
364+
- **Better file API**: `FileBuffer` class provides clean methods (`read()`, `seek()`, `close()`, `save_to()`) for working with uploaded files
365+
- **OpenAPI documentation**: `FromText` and `FromFiles` are now fully documented in OpenAPI schemas
366+
367+
///
368+
369+
### FromForm: Application/x-www-form-urlencoded and Multipart
370+
371+
The `FromForm` binder handles both `application/x-www-form-urlencoded` and `multipart/form-data` content types:
372+
373+
```python
374+
from dataclasses import dataclass
375+
from blacksheep import FromForm, post
376+
377+
378+
@dataclass
379+
class UserInput:
380+
name: str
381+
email: str
382+
age: int
383+
384+
385+
@post("/users")
386+
async def create_user(input: FromForm[UserInput]):
387+
user = input.value
388+
# user.name, user.email, user.age are automatically parsed
389+
return {"message": f"User {user.name} created"}
390+
```
391+
392+
### FromFiles: File Upload Handling
393+
394+
The `FromFiles` binder provides efficient handling of file uploads with memory-efficient buffering:
395+
396+
```python
397+
from blacksheep import FromFiles, post
398+
399+
400+
@post("/upload")
401+
async def upload_files(files: FromFiles):
402+
# files.value is a list of FormPart objects
403+
for file_part in files.value:
404+
# Access file metadata
405+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
406+
content_type = file_part.content_type.decode() if file_part.content_type else None
407+
408+
# file_part.file is a FormPart instance with efficient memory handling
409+
# Small files (<1MB) are kept in memory, larger files use temporary disk files
410+
file_buffer = file_part.file
411+
412+
# Or save directly to disk (recommended for large files)
413+
await file_buffer.save_to(f"./uploads/{file_name}")
414+
415+
return {"uploaded": len(files.value)}
416+
```
417+
418+
### Mixed Multipart Forms: Files and Text Together
419+
420+
Since version 2.6.0, you can use `FromText` and `FromFiles` together to handle forms with both text fields and file uploads:
421+
422+
```python
423+
from blacksheep import FromFiles, FromText, post
424+
425+
426+
@post("/upload-with-description")
427+
async def upload_with_metadata(
428+
description: FromText,
429+
files: FromFiles,
430+
):
431+
# description.value contains the text field value
432+
text_content = description.value
433+
434+
# files.value contains the uploaded files
435+
for file_part in files.value:
436+
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
437+
438+
# Process the file
439+
await file_part.file.save_to(f"./uploads/{file_name}")
440+
441+
return {
442+
"description": text_content,
443+
"files_count": len(files.value)
444+
}
445+
```
446+
447+
### Memory-Efficient Streaming for Large Files
448+
449+
For handling very large file uploads efficiently, use the streaming API directly:
450+
451+
```python
452+
from blacksheep import post, Request, created
453+
454+
455+
@post("/upload-large")
456+
async def upload_large_files(request: Request):
457+
# Stream multipart data without buffering entire request body
458+
async for part in request.multipart_stream():
459+
if part.file_name:
460+
# This is a file upload
461+
file_name = part.file_name.decode()
462+
463+
# Stream the file content in chunks
464+
with open(f"./uploads/{file_name}", "wb") as f:
465+
async for chunk in part.stream():
466+
f.write(chunk)
467+
else:
468+
# This is a regular form field
469+
field_name = part.name.decode() if part.name else ""
470+
field_value = part.data.decode()
471+
472+
return created()
473+
```
474+
475+
### Automatic Resource Cleanup
476+
477+
BlackSheep automatically manages file resources. The framework calls `Request.dispose()` at the end of each request-response cycle to clean up temporary files created during multipart parsing. This ensures:
478+
479+
- Temporary files are automatically deleted
480+
- Memory is properly released
481+
- No manual cleanup is required in most cases
482+
483+
If you need manual control over resource cleanup (e.g., in long-running background tasks), you can call `request.dispose()` explicitly.
484+
353485
## Defining a custom binder
354486

355487
To define a custom binder, define a `BoundValue[T]` class and a `Binder`

blacksheep/docs/requests.md

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,7 @@ Files are read from `multipart/form-data` payload.
297297
# Access file metadata
298298
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
299299
content_type = file_part.content_type.decode() if file_part.content_type else None
300-
301-
# file_part.file is a FileBuffer instance with efficient memory handling
302-
# Small files (<1MB) are kept in memory, larger files use temporary disk files
303-
file_buffer = file_part.file
304-
305-
# Read file content
306-
content = file_buffer.read()
307-
300+
308301
# Or save directly to disk
309302
await file_buffer.save_to(f"./uploads/{file_name}")
310303
```
@@ -322,10 +315,10 @@ Files are read from `multipart/form-data` payload.
322315
for part in files:
323316
# Access file metadata
324317
file_name = part.file_name.decode() if part.file_name else "unknown"
325-
318+
326319
# file_bytes contains the entire file content
327320
file_bytes = part.data
328-
321+
329322
# Or use the FileBuffer for more control
330323
file_buffer = part.file
331324
content = file_buffer.read()
@@ -346,7 +339,7 @@ Files are read from `multipart/form-data` payload.
346339
if part.file_name:
347340
# This is a file upload
348341
file_name = part.file_name.decode()
349-
342+
350343
# Stream the file content in chunks
351344
with open(f"./uploads/{file_name}", "wb") as f:
352345
async for chunk in part.stream():
@@ -356,7 +349,7 @@ Files are read from `multipart/form-data` payload.
356349
field_name = part.name.decode() if part.name else ""
357350
field_value = part.data.decode()
358351
print(f"Field {field_name}: {field_value}")
359-
352+
360353
return created()
361354
```
362355

@@ -375,14 +368,14 @@ Files are read from `multipart/form-data` payload.
375368
):
376369
# description.value contains the text field value
377370
text_content = description.value
378-
371+
379372
# files.value contains the uploaded files
380373
for file_part in files.value:
381374
file_name = file_part.file_name.decode() if file_part.file_name else "unknown"
382-
375+
383376
# Process the file
384377
await file_part.file.save_to(f"./uploads/{file_name}")
385-
378+
386379
return {"description": text_content, "files_count": len(files.value)}
387380
```
388381

@@ -398,7 +391,7 @@ from blacksheep import post, Request
398391
async def manual_file_handling(request: Request):
399392
try:
400393
files = await request.files()
401-
394+
402395
for part in files:
403396
# Process files
404397
pass
@@ -425,16 +418,16 @@ from blacksheep import FromFiles, post
425418
async def process_file(files: FromFiles):
426419
for file_part in files.value:
427420
file_buffer = file_part.file
428-
421+
429422
# Read first 100 bytes
430423
header = file_buffer.read(100)
431-
424+
432425
# Go back to start
433426
file_buffer.seek(0)
434-
427+
435428
# Read entire content
436429
full_content = file_buffer.read()
437-
430+
438431
# Save to disk
439432
await file_buffer.save_to("./output.bin")
440433
```

0 commit comments

Comments
 (0)