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_LENGTH set 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.