From 9894481f3cb42f46bac7fd827338e25bc1db47d8 Mon Sep 17 00:00:00 2001 From: brunofoggiatto Date: Mon, 10 Nov 2025 10:27:47 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20file=20upload=20validation?= =?UTF-8?q?=20and=20saving=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/en/docs/tutorial/request-files.md | 54 ++++++++++++++++++ docs_src/request_files/tutorial004.py | 43 +++++++++++++++ .../request_files/tutorial004_an_py310.py | 43 +++++++++++++++ docs_src/request_files/tutorial004_an_py39.py | 43 +++++++++++++++ docs_src/request_files/tutorial005.py | 55 +++++++++++++++++++ .../request_files/tutorial005_an_py310.py | 55 +++++++++++++++++++ docs_src/request_files/tutorial005_an_py39.py | 55 +++++++++++++++++++ 7 files changed, 348 insertions(+) create mode 100644 docs_src/request_files/tutorial004.py create mode 100644 docs_src/request_files/tutorial004_an_py310.py create mode 100644 docs_src/request_files/tutorial004_an_py39.py create mode 100644 docs_src/request_files/tutorial005.py create mode 100644 docs_src/request_files/tutorial005_an_py310.py create mode 100644 docs_src/request_files/tutorial005_an_py39.py diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 3d6e9c18a..a4cc1ef97 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -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. diff --git a/docs_src/request_files/tutorial004.py b/docs_src/request_files/tutorial004.py new file mode 100644 index 000000000..7dfcf2875 --- /dev/null +++ b/docs_src/request_files/tutorial004.py @@ -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} diff --git a/docs_src/request_files/tutorial004_an_py310.py b/docs_src/request_files/tutorial004_an_py310.py new file mode 100644 index 000000000..fa1fa61b3 --- /dev/null +++ b/docs_src/request_files/tutorial004_an_py310.py @@ -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} diff --git a/docs_src/request_files/tutorial004_an_py39.py b/docs_src/request_files/tutorial004_an_py39.py new file mode 100644 index 000000000..fa1fa61b3 --- /dev/null +++ b/docs_src/request_files/tutorial004_an_py39.py @@ -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} diff --git a/docs_src/request_files/tutorial005.py b/docs_src/request_files/tutorial005.py new file mode 100644 index 000000000..345e30171 --- /dev/null +++ b/docs_src/request_files/tutorial005.py @@ -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} diff --git a/docs_src/request_files/tutorial005_an_py310.py b/docs_src/request_files/tutorial005_an_py310.py new file mode 100644 index 000000000..cc2c96b60 --- /dev/null +++ b/docs_src/request_files/tutorial005_an_py310.py @@ -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} diff --git a/docs_src/request_files/tutorial005_an_py39.py b/docs_src/request_files/tutorial005_an_py39.py new file mode 100644 index 000000000..cc2c96b60 --- /dev/null +++ b/docs_src/request_files/tutorial005_an_py39.py @@ -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}