backend
beginner
File Uploads with Validation
min read
Frederick Tubiermont
File Uploads with Validation
File uploads are common — profile photos, CSV imports, document attachments. Here's how to handle them safely.
HTML Form
File uploads require enctype="multipart/form-data":
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="file" accept=".jpg,.jpeg,.png,.pdf">
<button type="submit">Upload</button>
</form>
Basic Upload Route
from flask import request, redirect, render_template
from werkzeug.utils import secure_filename
import os
UPLOAD_FOLDER = "static/uploads"
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "pdf"}
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
# Create upload directory if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename: str) -> bool:
"""Check if file extension is allowed."""
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/upload", methods=["POST"])
@login_required
def upload_file():
if "file" not in request.files:
return render_template("upload.html", error="No file selected")
file = request.files["file"]
if file.filename == "":
return render_template("upload.html", error="No file selected")
if not allowed_file(file.filename):
return render_template("upload.html", error="File type not allowed. Use: jpg, png, gif, pdf")
# Check file size
file.seek(0, 2) # Seek to end
size = file.tell()
file.seek(0) # Reset to beginning
if size > MAX_FILE_SIZE:
return render_template("upload.html", error="File too large. Maximum 5MB")
# secure_filename strips path traversal attacks (../../etc/passwd)
filename = secure_filename(file.filename)
# Add user ID prefix to avoid filename collisions
user_id = session["user_id"]
unique_filename = f"{user_id}_{filename}"
file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
file.save(file_path)
# Store path in database
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO uploads (user_id, filename, file_path, size) VALUES (%s, %s, %s, %s) RETURNING id",
(user_id, filename, file_path, size)
)
upload_id = cur.fetchone()["id"]
conn.commit()
return redirect(f"/files/{upload_id}")
Configure Max Upload Size in Flask
# app.py or config.py
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # 5MB
# Flask will automatically return 413 if exceeded
Database Table
CREATE TABLE uploads (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
filename VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
size INTEGER NOT NULL,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Serving Uploaded Files
from flask import send_from_directory
@app.route("/files/<int:upload_id>")
@login_required
def serve_file(upload_id):
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT * FROM uploads WHERE id = %s AND user_id = %s",
(upload_id, session["user_id"])
)
upload = cur.fetchone()
if not upload:
return "File not found", 404
return send_from_directory(
UPLOAD_FOLDER,
os.path.basename(upload["file_path"]),
as_attachment=False
)
Using Unique Filenames to Prevent Collisions
import uuid
def save_file(file) -> str:
"""Save file with a UUID name. Returns the saved path."""
ext = file.filename.rsplit(".", 1)[1].lower()
unique_name = f"{uuid.uuid4().hex}.{ext}"
file_path = os.path.join(UPLOAD_FOLDER, unique_name)
file.save(file_path)
return file_path
Deleting Files
@app.route("/files/<int:upload_id>/delete", methods=["POST"])
@login_required
def delete_file(upload_id):
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT file_path FROM uploads WHERE id = %s AND user_id = %s",
(upload_id, session["user_id"])
)
upload = cur.fetchone()
if not upload:
return "File not found", 404
# Delete from disk
if os.path.exists(upload["file_path"]):
os.remove(upload["file_path"])
# Delete from database
with get_db() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM uploads WHERE id = %s", (upload_id,))
conn.commit()
return redirect("/files")
Security Checklist
- ✅
secure_filename()prevents path traversal - ✅ Extension whitelist (not blacklist) prevents malicious files
- ✅ File size check before saving
- ✅
MAX_CONTENT_LENGTHset in Flask config - ✅ Files served through Flask (not direct static URL) so you can enforce auth
- ✅ UUID or user-prefixed filenames prevent collisions and guessing
Was this helpful?
Get More Flask Vibe Tutorials
Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.
No spam. Unsubscribe anytime.