File Uploads
Table of Contents
- Overview
- Basic File Upload
- File Validation
- Multiple File Uploads
- File Upload with Form Data
- Saving Files to Disk
- Configuration
- Complete Examples
- Best Practices
Overview
Mitsuki provides built-in support for handling file uploads through multipart/form-data requests. The framework offers:
- Type-safe file handling -
UploadFileclass for uploaded files - Automatic validation - File type and size validation
- Form parameter mixing - Combine files with regular form fields
- Multiple file support - Upload multiple files in a single request
- Streaming support - Efficient handling of large files
- Easy file saving - Save files to disk with directory creation
Basic File Upload
Single File Upload
python
from mitsuki import RestController, PostMapping, FormFile, UploadFile
@RestController("/api/uploads")
class UploadController:
@PostMapping("/")
async def upload_file(self, file: UploadFile = FormFile()) -> dict:
"""Upload a single file."""
content = await file.read()
return {
"filename": file.filename,
"size": file.size,
"content_type": file.content_type,
}UploadFile Properties
The UploadFile object provides:
filename: str- Original filename from clientfile: BinaryIO- File-like object for readingcontent_type: Optional[str]- MIME type (e.g., "image/jpeg")size: int- File size in bytes
Reading File Content
python
@PostMapping("/process")
async def process_file(self, file: UploadFile = FormFile()) -> dict:
# Read entire file
content = await file.read()
# Read in chunks
chunk = await file.read(1024) # Read 1KB
# File is seekable
file.file.seek(0) # Reset to beginningFile Validation
File Type Validation
Restrict uploads to specific MIME types:
python
@PostMapping("/upload-image")
async def upload_image(
self,
image: UploadFile = FormFile(
allowed_types=["image/jpeg", "image/png", "image/gif"]
),
) -> dict:
"""Only accept JPEG, PNG, and GIF images."""
return {
"filename": image.filename,
"type": image.content_type,
"size": image.size,
}Invalid file types return HTTP 400:
json
{
"error": "File type application/pdf not allowed"
}File Size Validation
Set maximum file size per upload:
python
@PostMapping("/upload-avatar")
async def upload_avatar(
self,
avatar: UploadFile = FormFile(
max_size=2 * 1024 * 1024, # 2MB limit
allowed_types=["image/jpeg", "image/png"]
),
) -> dict:
"""Upload avatar with 2MB size limit."""
await avatar.save(f"avatars/{avatar.filename}")
return {"message": "Avatar uploaded"}Files exceeding the limit return HTTP 400:
json
{
"error": "File avatar.jpg size 3145728 exceeds maximum 2097152 bytes"
}Combined Validation
python
@PostMapping("/upload-document")
async def upload_document(
self,
document: UploadFile = FormFile(
allowed_types=["application/pdf", "application/msword"],
max_size=10 * 1024 * 1024, # 10MB
),
) -> dict:
"""Upload PDF or Word document up to 10MB."""
return {"filename": document.filename}Optional Files
Make file uploads optional:
python
@PostMapping("/create-post")
async def create_post(
self,
title: str = FormParam(),
image: Optional[UploadFile] = FormFile(required=False),
) -> dict:
"""Create post with optional image."""
result = {"title": title}
if image:
await image.save(f"posts/{image.filename}")
result["image"] = image.filename
return resultMultiple File Uploads
Upload Multiple Files
python
from typing import List
@PostMapping("/upload-gallery")
async def upload_gallery(
self,
images: List[UploadFile] = FormFile(
allowed_types=["image/jpeg", "image/png"]
),
) -> dict:
"""Upload multiple images at once."""
uploaded = []
for image in images:
await image.save(f"gallery/{image.filename}")
uploaded.append({
"filename": image.filename,
"size": image.size,
})
return {
"count": len(images),
"files": uploaded,
}Client Request Example
Upload multiple files using the same field name:
bash
curl -X POST http://localhost:8000/api/upload-gallery \
-F "images=@photo1.jpg" \
-F "images=@photo2.jpg" \
-F "images=@photo3.png"File Upload with Form Data
Mixing Files and Form Fields
Combine file uploads with regular form parameters:
python
@PostMapping("/upload-with-metadata")
async def upload_with_metadata(
self,
file: UploadFile = FormFile(),
title: str = FormParam(),
description: str = FormParam(default=""),
tags: str = FormParam(default=""),
) -> dict:
"""Upload file with additional metadata."""
await file.save(f"uploads/{file.filename}")
return {
"filename": file.filename,
"title": title,
"description": description,
"tags": tags.split(",") if tags else [],
}Client Request
bash
curl -X POST http://localhost:8000/api/upload-with-metadata \
-F "file=@document.pdf" \
-F "title=My Document" \
-F "description=Important file" \
-F "tags=work,2024"Multiple Files with Metadata
python
@PostMapping("/upload-post")
async def upload_post(
self,
title: str = FormParam(),
content: str = FormParam(),
images: List[UploadFile] = FormFile(required=False),
attachments: List[UploadFile] = FormFile(required=False),
) -> dict:
"""Upload post with multiple image and attachment fields."""
result = {
"title": title,
"content": content,
"images": [],
"attachments": [],
}
if images:
for img in images:
await img.save(f"posts/images/{img.filename}")
result["images"].append(img.filename)
if attachments:
for att in attachments:
await att.save(f"posts/attachments/{att.filename}")
result["attachments"].append(att.filename)
return resultSaving Files to Disk
Basic File Save
python
@PostMapping("/upload")
async def upload(self, file: UploadFile = FormFile()) -> dict:
"""Save uploaded file to disk."""
# Save to specific path
await file.save("/var/uploads/myfile.pdf")
return {"message": "File saved"}Configuration
Global Upload Limits
Configure upload limits in application.yml:
yaml
server:
max_body_size: 10485760 # 10MB total request size
multipart:
max_file_size: 10485760 # 10MB per file (default)
max_request_size: 52428800 # 50MB total request (default)Configuration Options
| Option | Default | Description |
|---|---|---|
server.max_body_size | 10MB | Maximum request body size |
server.multipart.max_file_size | 10MB | Maximum size per file |
server.multipart.max_request_size | 50MB | Maximum total request size |
Environment Variables
Override via environment variables:
bash
# Set max file size to 20MB
MITSUKI_SERVER_MULTIPART_MAX_FILE_SIZE=20971520
# Set max request size to 100MB
MITSUKI_SERVER_MULTIPART_MAX_REQUEST_SIZE=104857600Best Practices
Security
- Validate file types - Always use
allowed_typesto restrict uploads - Set size limits - Use
max_sizeto prevent large file attacks - Sanitize filenames - Don't trust client-provided filenames
- Scan for malware - Consider virus scanning for user uploads
Performance
- Stream large files - Use
file.read(chunk_size)for large files - Set appropriate limits - Configure
max_file_sizebased on your needs - Use async I/O - Use
await file.save()for non-blocking I/O - Consider cloud storage - For production, consider using S3, GCS, or Azure Blob Storage
Error Handling
python
from mitsuki.exceptions import FileTooLargeException, InvalidFileTypeException
@PostMapping("/upload")
async def upload(self, file: UploadFile = FormFile()) -> dict:
try:
await file.save(f"uploads/{file.filename}")
return {"message": "Success"}
except FileTooLargeException as e:
return ResponseEntity.bad_request({"error": str(e)})
except InvalidFileTypeException as e:
return ResponseEntity.bad_request({"error": str(e)})
except Exception as e:
return ResponseEntity.server_error({"error": "Upload failed"})Validation Examples
python
# Images only
image: UploadFile = FormFile(
allowed_types=["image/jpeg", "image/png", "image/gif"]
)
# Documents only
doc: UploadFile = FormFile(
allowed_types=[
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]
)
# Small avatar
avatar: UploadFile = FormFile(
allowed_types=["image/jpeg", "image/png"],
max_size=2 * 1024 * 1024, # 2MB
)
# Large video
video: UploadFile = FormFile(
allowed_types=["video/mp4", "video/quicktime"],
max_size=100 * 1024 * 1024, # 100MB
)Next Steps
- Controllers - REST API development
- Request/Response Validation - Data validation
- Configuration - Configure upload limits