mirror of https://github.com/tiangolo/fastapi.git
📝 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:
parent
409e7b503c
commit
9894481f3c
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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}
|
||||
Loading…
Reference in New Issue