📝 Add file upload validation and saving examples

This PR adds practical examples for validating and saving uploaded files,
addressing common production use cases.

New examples:
- Tutorial 004: Upload with validation (file type, size, count)
- Tutorial 005: Saving files to disk with unique names

Each example includes variants for Python 3.9+ and 3.10+.

Documentation updated with new sections explaining validation
best practices and file saving security considerations.
This commit is contained in:
brunofoggiatto 2025-11-10 10:27:47 -03:00
parent 409e7b503c
commit 9894481f3c
7 changed files with 348 additions and 0 deletions

View File

@ -171,6 +171,60 @@ And the same way as before, you can use `File()` to set additional parameters, e
{* ../../docs_src/request_files/tutorial003_an_py39.py hl[11,18:20] *}
## File Upload with Validation { #file-upload-with-validation }
In production applications, you often need to validate uploaded files to ensure they meet certain requirements.
Here's an example that validates:
* **File type**: Only allows specific image formats (JPEG, PNG, GIF, WebP)
* **File size**: Maximum 5MB per file
* **Number of files**: Maximum 10 files per request
{* ../../docs_src/request_files/tutorial004_an_py39.py hl[9:13,22:24,26:30,33:50] *}
/// tip
Notice how we:
1. Check the `content_type` attribute to validate the file type
2. Read the file contents with `await file.read()` to check the size
3. Use `await file.seek(0)` to reset the file pointer for potential further processing
4. Raise `HTTPException` with appropriate status codes and clear error messages
///
/// warning
Be careful when reading file contents to check the size. For very large files, this could consume significant memory. Consider using streaming validation for production applications with very large file uploads.
///
## Saving Uploaded Files { #saving-uploaded-files }
When you need to save uploaded files to disk, you can use Python's built-in file operations along with `shutil` for efficient copying.
Here's a practical example of uploading and saving product images:
{* ../../docs_src/request_files/tutorial005_an_py39.py hl[1:4,10:12,16:20,53:59,61:63] *}
/// tip
**Security best practices:**
* Generate unique filenames using `uuid4()` to avoid conflicts and potential security issues
* Validate file extensions to prevent malicious uploads
* Store files outside your application's source code directory
* Consider using a dedicated storage service (like AWS S3, Google Cloud Storage) for production applications
///
/// note | Technical Details
The `shutil.copyfileobj()` function efficiently copies the file content from the `UploadFile.file` object to the destination file. This is more memory-efficient than reading the entire file into memory first.
///
## Recap { #recap }
Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data.

View File

@ -0,0 +1,43 @@
from typing import List
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
@app.post("/upload-images/")
async def upload_images(
files: List[UploadFile] = File(description="Multiple images"),
):
allowed_types = ["image/jpeg", "image/png", "image/gif", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
results = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
await file.seek(0)
results.append(
{
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
)
return {"uploaded": len(results), "files": results}

View File

@ -0,0 +1,43 @@
from typing import Annotated
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
@app.post("/upload-images/")
async def upload_images(
files: Annotated[list[UploadFile], File(description="Multiple images")],
):
allowed_types = ["image/jpeg", "image/png", "image/gif", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
results = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
await file.seek(0)
results.append(
{
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
)
return {"uploaded": len(results), "files": results}

View File

@ -0,0 +1,43 @@
from typing import Annotated
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
@app.post("/upload-images/")
async def upload_images(
files: Annotated[list[UploadFile], File(description="Multiple images")],
):
allowed_types = ["image/jpeg", "image/png", "image/gif", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
results = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
await file.seek(0)
results.append(
{
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
)
return {"uploaded": len(results), "files": results}

View File

@ -0,0 +1,55 @@
import shutil
from pathlib import Path
from typing import List
from uuid import uuid4
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
UPLOAD_DIR = Path("uploaded_files")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload-product-images/")
async def upload_product_images(
files: List[UploadFile] = File(description="Product images"),
):
allowed_types = ["image/jpeg", "image/png", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
saved = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
file_ext = Path(file.filename).suffix
unique_name = f"{uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_name
await file.seek(0)
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
saved.append(
{
"filename": file.filename,
"saved_as": unique_name,
"size": len(contents),
}
)
return {"uploaded": len(saved), "files": saved}

View File

@ -0,0 +1,55 @@
import shutil
from pathlib import Path
from typing import Annotated
from uuid import uuid4
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
UPLOAD_DIR = Path("uploaded_files")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload-product-images/")
async def upload_product_images(
files: Annotated[list[UploadFile], File(description="Product images")],
):
allowed_types = ["image/jpeg", "image/png", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
saved = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
file_ext = Path(file.filename).suffix
unique_name = f"{uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_name
await file.seek(0)
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
saved.append(
{
"filename": file.filename,
"saved_as": unique_name,
"size": len(contents),
}
)
return {"uploaded": len(saved), "files": saved}

View File

@ -0,0 +1,55 @@
import shutil
from pathlib import Path
from typing import Annotated
from uuid import uuid4
from fastapi import FastAPI, File, HTTPException, UploadFile
app = FastAPI()
UPLOAD_DIR = Path("uploaded_files")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload-product-images/")
async def upload_product_images(
files: Annotated[list[UploadFile], File(description="Product images")],
):
allowed_types = ["image/jpeg", "image/png", "image/webp"]
max_size = 5 * 1024 * 1024 # 5MB
if len(files) > 10:
raise HTTPException(status_code=400, detail="Too many files")
saved = []
for file in files:
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.content_type}",
)
contents = await file.read()
if len(contents) > max_size:
raise HTTPException(
status_code=400, detail=f"File too large: {file.filename}"
)
file_ext = Path(file.filename).suffix
unique_name = f"{uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_name
await file.seek(0)
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
saved.append(
{
"filename": file.filename,
"saved_as": unique_name,
"size": len(contents),
}
)
return {"uploaded": len(saved), "files": saved}