File Uploads¶
Cello handles multipart form data and file uploads through its Rust-powered multer integration. Files are parsed efficiently in Rust and exposed to Python as UploadedFile objects with methods for reading, saving, and inspecting uploaded content.
Basic File Upload¶
Single File¶
from cello import App
app = App()
@app.post("/upload")
def upload_file(request):
form = request.form()
file = form.get_file("document")
if file is None:
return {"error": "No file uploaded"}, 400
return {
"filename": file.filename,
"content_type": file.content_type,
"size": file.size(),
}
HTML Form¶
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="document">
<button type="submit">Upload</button>
</form>
The UploadedFile Class¶
Each uploaded file is represented as an UploadedFile object with the following interface:
Properties¶
| Property | Type | Description |
|---|---|---|
filename | str | Original filename from the client |
content_type | str | MIME type (e.g., "image/png") |
temp_path | str | None | Temporary file path, if saved to disk |
Methods¶
| Method | Return Type | Description |
|---|---|---|
read() | bytes | Get the file content as raw bytes |
read_text() | str | Get the file content as UTF-8 text |
size() | int | Get the file size in bytes |
save(path) | None | Save the file to the specified filesystem path |
extension() | str | None | Get the file extension (e.g., "png") |
Example¶
@app.post("/upload")
def upload(request):
form = request.form()
file = form.get_file("avatar")
if file is None:
return {"error": "No file provided"}, 400
# Inspect the file
print(f"Name: {file.filename}") # "photo.jpg"
print(f"Type: {file.content_type}") # "image/jpeg"
print(f"Size: {file.size()} bytes") # 245760
print(f"Ext: {file.extension()}") # "jpg"
# Read content
raw_bytes = file.read()
text_content = file.read_text() # Only for text files
# Save to disk
file.save(f"./uploads/{file.filename}")
return {"saved": file.filename, "size": file.size()}
The FormData Class¶
Multipart forms can contain both text fields and file fields. The FormData class provides access to both:
Text Fields¶
@app.post("/profile")
def update_profile(request):
form = request.form()
# Get text fields
name = form.get("name") # Returns str or None
bio = form.get_or("bio", "No bio set") # Returns str with default
# Check field existence
if form.has_field("email"):
email = form.get("email")
# List all field names
field_names = form.field_names() # ["name", "bio", "email"]
field_count = form.field_count() # 3
return {"name": name, "bio": bio}
File Fields¶
@app.post("/documents")
def upload_documents(request):
form = request.form()
# Get a single file
file = form.get_file("document")
# Check file existence
if form.has_file("attachment"):
attachment = form.get_file("attachment")
# List all file field names
file_names = form.file_names() # ["document", "attachment"]
file_count = form.file_count() # 2
return {"files_received": file_count}
Multiple File Uploads¶
Multiple Files in One Field¶
When a form field accepts multiple files (e.g., <input type="file" multiple>), use get_files() to retrieve all of them:
@app.post("/gallery")
def upload_gallery(request):
form = request.form()
# Get all files from the "photos" field
photos = form.get_files("photos")
results = []
for photo in photos:
photo.save(f"./uploads/gallery/{photo.filename}")
results.append({
"filename": photo.filename,
"size": photo.size(),
})
return {"uploaded": len(results), "files": results}
HTML form:
<form action="/gallery" method="POST" enctype="multipart/form-data">
<input type="file" name="photos" multiple>
<button type="submit">Upload Photos</button>
</form>
Multiple File Fields¶
@app.post("/application")
def submit_application(request):
form = request.form()
# Text fields
name = form.get("name")
email = form.get("email")
# Different file fields
resume = form.get_file("resume")
cover_letter = form.get_file("cover_letter")
portfolio = form.get_files("portfolio") # Multiple files
if resume:
resume.save(f"./uploads/resumes/{resume.filename}")
return {
"applicant": name,
"resume": resume.filename if resume else None,
"portfolio_count": len(portfolio),
}
File Size Limits¶
Default Limits¶
Cello enforces default size limits to prevent abuse:
| Limit | Default |
|---|---|
| Maximum file size | 10 MB |
| Maximum total form size | 50 MB |
Body Limit Middleware¶
Use the body limit middleware to customize request size limits:
from cello import App
app = App()
# Set maximum request body to 25 MB
app.enable_body_limit(25 * 1024 * 1024)
Requests exceeding the limit receive a 413 Payload Too Large response before the body is fully read.
Validating File Size in Handlers¶
You can also validate file size within your handler for more granular control:
MAX_AVATAR_SIZE = 2 * 1024 * 1024 # 2 MB
@app.post("/avatar")
def upload_avatar(request):
form = request.form()
file = form.get_file("avatar")
if file is None:
return Response.json({"error": "No file"}, status=400)
if file.size() > MAX_AVATAR_SIZE:
return Response.json(
{"error": f"File too large. Max {MAX_AVATAR_SIZE // (1024*1024)} MB"},
status=413,
)
# Validate content type
allowed_types = ["image/jpeg", "image/png", "image/webp"]
if file.content_type not in allowed_types:
return Response.json(
{"error": f"Invalid file type: {file.content_type}"},
status=415,
)
file.save(f"./uploads/avatars/{file.filename}")
return {"uploaded": file.filename}
URL-Encoded Forms¶
For non-file forms (application/x-www-form-urlencoded), Cello parses the body automatically:
@app.post("/login")
def login(request):
form = request.form()
username = form.get("username")
password = form.get("password")
if authenticate(username, password):
return {"token": generate_token(username)}
return Response.json({"error": "Invalid credentials"}, status=401)
HTML form:
<form action="/login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">Login</button>
</form>
Practical Example: Image Upload Service¶
from cello import App, Response
import os
import uuid
app = App()
app.enable_body_limit(20 * 1024 * 1024) # 20 MB max
UPLOAD_DIR = "./uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
@app.post("/images")
def upload_image(request):
form = request.form()
file = form.get_file("image")
if not file:
return Response.json({"error": "No image provided"}, status=400)
if file.content_type not in ALLOWED_TYPES:
return Response.json({"error": "Unsupported image type"}, status=415)
# Generate unique filename
ext = file.extension() or "bin"
unique_name = f"{uuid.uuid4().hex}.{ext}"
save_path = os.path.join(UPLOAD_DIR, unique_name)
file.save(save_path)
return {
"id": unique_name,
"original_name": file.filename,
"size": file.size(),
"content_type": file.content_type,
"url": f"/uploads/{unique_name}",
}
if __name__ == "__main__":
app.run()
Next Steps¶
- Static Files - Serve uploaded files back to clients
- DTOs & Validation - Validate form field values
- Body Limit Middleware - Configure request size limits